blob: 654a9f92d97bc2314a63bb2aa7ff1f1e4103ee6e [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.usbtuner.exoplayer;
import android.media.MediaCodec;
import android.media.MediaDataSource;
import android.os.ConditionVariable;
import android.os.SystemClock;
import android.util.Log;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.android.usbtuner.tvinput.PlaybackCacheListener;
import junit.framework.Assert;
import java.io.IOException;
import java.util.Locale;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* Extracts samples from {@link MediaDataSource} and stores them on the disk, which enables
* trickplay.
*/
public class CachedSampleSourceExtractor extends BaseSampleSourceExtractor implements
CacheManager.EvictListener {
private static final String TAG = "CachedSampleSourceExt";
private static final boolean DEBUG = false;
public static final long CHUNK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500);
private static final long LIVE_THRESHOLD_US = TimeUnit.SECONDS.toMicros(1);
private static final long CACHE_WRITE_TIMEOUT_MS = 10 * 1000; // 10 seconds
private final CacheManager mCacheManager;
private final String mId;
private final PlaybackCacheListener mCacheListener;
private long[] mCacheEndPositionUs;
private SampleCache[] mSampleCaches;
private CachedSampleQueue[] mPlayingSampleQueues;
private final SamplePool mSamplePool = new SamplePool();
private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US;
private long mCurrentPlaybackPositionUs = 0;
private class CachedSampleQueue extends SampleQueue {
private SampleCache mCache = null;
public CachedSampleQueue(SamplePool samplePool) {
super(samplePool);
}
public void setSource(SampleCache newCache) {
for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) {
cache.clear();
cache.close();
}
mCache = newCache;
for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) {
cache.resetRead();
}
}
public boolean maybeReadSample() {
if (isDurationGreaterThan(CHUNK_DURATION_US)) {
return false;
}
SampleHolder sample = mCache.maybeReadSample();
if (sample == null) {
if (!mCache.canReadMore() && mCache.getNext() != null) {
mCache.clear();
mCache.close();
mCache = mCache.getNext();
mCache.resetRead();
return maybeReadSample();
} else {
return false;
}
} else {
queueSample(sample);
return true;
}
}
public int dequeueSample(SampleHolder sample) {
maybeReadSample();
return super.dequeueSample(sample);
}
@Override
public void clear() {
super.clear();
for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) {
cache.clear();
cache.close();
}
mCache = null;
}
public long getSourceStartPositionUs() {
return mCache == null ? -1 : mCache.getStartPositionUs();
}
}
public CachedSampleSourceExtractor(MediaDataSource source, CacheManager cacheManager,
PlaybackCacheListener cacheListener) {
super(source);
mCacheManager = cacheManager;
mCacheListener = cacheListener;
mId = Long.toHexString(new Random().nextLong());
cacheListener.onCacheStateChanged(true); // Enable trickplay
}
@Override
public void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
throws IOException {
long writeStartTimeNs = SystemClock.elapsedRealtimeNanos();
synchronized (this) {
SampleCache cache = mSampleCaches[index];
if ((sample.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
if (sample.timeUs >= mCacheEndPositionUs[index]) {
try {
SampleCache nextCache = mCacheManager.createNewWriteFile(
getTrackId(index), mCacheEndPositionUs[index], mSamplePool);
cache.finishWrite(nextCache);
mSampleCaches[index] = cache = nextCache;
mCacheEndPositionUs[index] =
((sample.timeUs / CHUNK_DURATION_US) + 1) * CHUNK_DURATION_US;
} catch (IOException e) {
cache.finishWrite(null);
throw e;
}
}
}
cache.writeSample(sample, conditionVariable);
}
if (!conditionVariable.block(CACHE_WRITE_TIMEOUT_MS)) {
Log.e(TAG, "Error: Serious delay on writing cache");
conditionVariable.block();
}
// Check if the storage has enough bandwidth for trickplay. Otherwise we disable it
// and notify the slowness through the playback cache listener.
mCacheManager.addWriteStat(sample.size,
SystemClock.elapsedRealtimeNanos() - writeStartTimeNs);
if (mCacheManager.isWriteSlow()) {
Log.w(TAG, "Disk is too slow for trickplay. Disable trickplay.");
mCacheManager.disable();
mCacheListener.onDiskTooSlow();
}
}
private String getTrackId(int index) {
return String.format(Locale.ENGLISH, "%s_%x", mId, index);
}
@Override
public void initOnPrepareLocked(int trackCount) throws IOException {
mSampleCaches = new SampleCache[trackCount];
mPlayingSampleQueues = new CachedSampleQueue[trackCount];
mCacheEndPositionUs = new long[trackCount];
for (int i = 0; i < trackCount; i++) {
mSampleCaches[i] = mCacheManager.createNewWriteFile(getTrackId(i), 0, mSamplePool);
mPlayingSampleQueues[i] = null;
mCacheEndPositionUs[i] = CHUNK_DURATION_US;
}
}
@Override
public void selectTrack(int index) {
synchronized (this) {
if (mPlayingSampleQueues[index] == null) {
String trackId = getTrackId(index);
mPlayingSampleQueues[index] = new CachedSampleQueue(mSamplePool);
mCacheManager.registerEvictListener(trackId, this);
seekIndividualTrackLocked(index, mCurrentPlaybackPositionUs,
isLiveLocked(mCurrentPlaybackPositionUs));
mPlayingSampleQueues[index].maybeReadSample();
}
}
}
@Override
public void deselectTrack(int index) {
synchronized (this) {
if (mPlayingSampleQueues[index] != null) {
mPlayingSampleQueues[index].clear();
mPlayingSampleQueues[index] = null;
mCacheManager.unregisterEvictListener(getTrackId(index));
}
}
}
@Override
public long getBufferedPositionUs() {
synchronized (this) {
Long result = null;
for (CachedSampleQueue queue : mPlayingSampleQueues) {
if (queue == null) {
continue;
}
Long bufferedPositionUs = queue.getEndPositionUs();
if (bufferedPositionUs == null) {
continue;
}
if (result == null || result > bufferedPositionUs) {
result = bufferedPositionUs;
}
}
if (result == null) {
return mLastBufferedPositionUs;
} else {
return (mLastBufferedPositionUs = result);
}
}
}
@Override
public void seekTo(long positionUs) {
synchronized (this) {
boolean isLive = isLiveLocked(positionUs);
// Seek video track first
for (int i = 0; i < mPlayingSampleQueues.length; ++i) {
CachedSampleQueue queue = mPlayingSampleQueues[i];
if (queue == null) {
continue;
}
seekIndividualTrackLocked(i, positionUs, isLive);
if (DEBUG) {
Log.d(TAG, "start time = " + queue.getSourceStartPositionUs());
}
}
mLastBufferedPositionUs = positionUs;
}
}
private boolean isLiveLocked(long positionUs) {
Long livePositionUs = null;
for (SampleCache cache : mSampleCaches) {
if (livePositionUs == null || livePositionUs < cache.getEndPositionUs()) {
livePositionUs = cache.getEndPositionUs();
}
}
return (livePositionUs == null
|| Math.abs(livePositionUs - positionUs) < LIVE_THRESHOLD_US);
}
private void seekIndividualTrackLocked(int index, long positionUs, boolean isLive) {
CachedSampleQueue queue = mPlayingSampleQueues[index];
if (queue == null) {
return;
}
queue.clear();
if (isLive) {
queue.setSource(mSampleCaches[index]);
} else {
queue.setSource(mCacheManager.getReadFile(getTrackId(index), positionUs));
}
queue.maybeReadSample();
}
@Override
public int readSample(int track, SampleHolder sampleHolder) {
synchronized (this) {
CachedSampleQueue queue = mPlayingSampleQueues[track];
Assert.assertNotNull(queue);
queue.maybeReadSample();
int result = queue.dequeueSample(sampleHolder);
if (result != SampleSource.SAMPLE_READ && getEos()) {
return SampleSource.END_OF_STREAM;
}
return result;
}
}
@Override
public void cleanUpImpl() {
if (mSampleCaches == null) {
return;
}
for (int i = 0; i < mSampleCaches.length; ++i) {
mSampleCaches[i].finishWrite(null);
mCacheManager.unregisterEvictListener(getTrackId(i));
}
for (int i = 0; i < mSampleCaches.length; ++i) {
mCacheManager.clearTrack(getTrackId(i));
}
}
@Override
public boolean continueBuffering(long positionUs) {
synchronized (this) {
boolean hasSamples = true;
mCurrentPlaybackPositionUs = positionUs;
for (CachedSampleQueue queue : mPlayingSampleQueues) {
if (queue == null) {
continue;
}
queue.maybeReadSample();
if (queue.isEmpty()) {
hasSamples = false;
}
}
return hasSamples;
}
}
// CacheEvictListener
@Override
public void onCacheEvicted(String id, long createdTimeMs) {
mCacheListener.onCacheStartTimeChanged(
createdTimeMs + TimeUnit.MICROSECONDS.toMillis(CHUNK_DURATION_US));
}
}