aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/tuner/exoplayer/buffer
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/tuner/exoplayer/buffer')
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java280
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java209
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java23
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java24
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java115
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java1
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java8
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java85
8 files changed, 497 insertions, 248 deletions
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
index eb596e93..112e9dc4 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
@@ -25,13 +25,14 @@ import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer.SampleHolder;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.exoplayer.SampleExtractor;
import com.android.tv.util.Utils;
import java.io.File;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.ConcurrentModificationException;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
@@ -59,7 +60,8 @@ public class BufferManager {
private final SampleChunk.SampleChunkCreator mSampleChunkCreator;
// Maps from track name to a map which maps from starting position to {@link SampleChunk}.
- private final Map<String, SortedMap<Long, SampleChunk>> mChunkMap = new ArrayMap<>();
+ private final Map<String, SortedMap<Long, Pair<SampleChunk, Integer>>> mChunkMap =
+ new ArrayMap<>();
private final Map<String, Long> mStartPositionMap = new ArrayMap<>();
private final Map<String, ChunkEvictedListener> mEvictListeners = new ArrayMap<>();
private final StorageManager mStorageManager;
@@ -77,13 +79,11 @@ public class BufferManager {
}
};
- private volatile boolean mClosed = false;
private int mMinSampleSizeForSpeedCheck = MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK;
private long mTotalWriteSize;
private long mTotalWriteTimeNs;
private float mWriteBandwidth = 0.0f;
private volatile int mSpeedCheckCount;
- private boolean mDisabled = false;
public interface ChunkEvictedListener {
void onChunkEvicted(String id, long createdTimeMs);
@@ -174,6 +174,66 @@ public class BufferManager {
}
/**
+ * A Track format which will be loaded and saved from the permanent storage for recordings.
+ */
+ public static class TrackFormat {
+
+ /**
+ * The track id for the specified track. The track id will be used as a track identifier
+ * for recordings.
+ */
+ public final String trackId;
+
+ /**
+ * The {@link MediaFormat} for the specified track.
+ */
+ public final MediaFormat format;
+
+ /**
+ * Creates TrackFormat.
+ * @param trackId
+ * @param format
+ */
+ public TrackFormat(String trackId, MediaFormat format) {
+ this.trackId = trackId;
+ this.format = format;
+ }
+ }
+
+ /**
+ * A Holder for a sample position which will be loaded from the index file for recordings.
+ */
+ public static class PositionHolder {
+
+ /**
+ * The current sample position in microseconds.
+ * The position is identical to the PTS(presentation time stamp) of the sample.
+ */
+ public final long positionUs;
+
+ /**
+ * Base sample position for the current {@link SampleChunk}.
+ */
+ public final long basePositionUs;
+
+ /**
+ * The file offset for the current sample in the current {@link SampleChunk}.
+ */
+ public final int offset;
+
+ /**
+ * Creates a holder for a specific position in the recording.
+ * @param positionUs
+ * @param offset
+ */
+ public PositionHolder(long positionUs, long basePositionUs, int offset) {
+ this.positionUs = positionUs;
+ this.basePositionUs = basePositionUs;
+ this.offset = offset;
+ }
+ }
+
+ /**
* Storage configuration and policy manager for {@link BufferManager}
*/
public interface StorageManager {
@@ -186,11 +246,6 @@ public class BufferManager {
File getBufferDir();
/**
- * Cleans up storage.
- */
- void clearStorage();
-
- /**
* Informs whether the storage is used for persistent use. (eg. dvr recording/play)
*
* @return {@code true} if stored files are persistent
@@ -220,29 +275,27 @@ public class BufferManager {
* Reads track name & {@link MediaFormat} from storage.
*
* @param isAudio {@code true} if it is for audio track
- * @return {@link Pair} of track name & {@link MediaFormat}
- * @throws IOException
+ * @return {@link List} of TrackFormat
*/
- Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException;
+ List<TrackFormat> readTrackInfoFiles(boolean isAudio);
/**
- * Reads sample indexes for each written sample from storage.
+ * Reads key sample positions for each written sample from storage.
*
* @param trackId track name
* @return indexes of the specified track
* @throws IOException
*/
- ArrayList<Long> readIndexFile(String trackId) throws IOException;
+ ArrayList<PositionHolder> readIndexFile(String trackId) throws IOException;
/**
* Writes track information to storage.
*
- * @param trackId track name
- * @param format {@link android.media.MediaFormat} of the track
+ * @param formatList {@list List} of TrackFormat
* @param isAudio {@code true} if it is for audio track
* @throws IOException
*/
- void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio)
+ void writeTrackInfoFiles(List<TrackFormat> formatList, boolean isAudio)
throws IOException;
/**
@@ -252,7 +305,7 @@ public class BufferManager {
* @param index {@link SampleChunk} container
* @throws IOException
*/
- void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index)
+ void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)
throws IOException;
}
@@ -307,7 +360,6 @@ public class BufferManager {
SampleChunk.SampleChunkCreator sampleChunkCreator) {
mStorageManager = storageManager;
mSampleChunkCreator = sampleChunkCreator;
- clearBuffer(true);
}
public void registerChunkEvictedListener(String id, ChunkEvictedListener listener) {
@@ -318,44 +370,44 @@ public class BufferManager {
mEvictListeners.remove(id);
}
- private void clearBuffer(boolean deleteFiles) {
- mChunkMap.clear();
- if (deleteFiles) {
- mStorageManager.clearStorage();
- }
- mBufferSize = 0;
- }
-
private static String getFileName(String id, long positionUs) {
return String.format(Locale.ENGLISH, "%s_%016x.chunk", id, positionUs);
}
/**
- * Creates a new {@link SampleChunk} for caching samples.
+ * Creates a new {@link SampleChunk} for caching samples if it is needed.
*
* @param id the name of the track
- * @param positionUs starting position of the {@link SampleChunk} in micro seconds.
+ * @param positionUs current position to write a sample in micro seconds.
* @param samplePool {@link SamplePool} for the fast creation of samples.
+ * @param currentChunk the current {@link SampleChunk} to write, {@code null} when to create
+ * a new {@link SampleChunk}.
+ * @param currentOffset the current offset to write.
* @return returns the created {@link SampleChunk}.
* @throws IOException
*/
- public SampleChunk createNewWriteFile(String id, long positionUs,
- SamplePool samplePool) throws IOException {
+ public SampleChunk createNewWriteFileIfNeeded(String id, long positionUs, SamplePool samplePool,
+ SampleChunk currentChunk, int currentOffset) throws IOException {
if (!maybeEvictChunk()) {
throw new IOException("Not enough storage space");
}
- SortedMap<Long, SampleChunk> map = mChunkMap.get(id);
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id);
if (map == null) {
map = new TreeMap<>();
mChunkMap.put(id, map);
mStartPositionMap.put(id, positionUs);
mPendingDelete.init(id);
}
- File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs));
- SampleChunk sampleChunk = mSampleChunkCreator.createSampleChunk(samplePool, file,
- positionUs, mChunkCallback);
- map.put(positionUs, sampleChunk);
- return sampleChunk;
+ if (currentChunk == null) {
+ File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs));
+ SampleChunk sampleChunk = mSampleChunkCreator
+ .createSampleChunk(samplePool, file, positionUs, mChunkCallback);
+ map.put(positionUs, new Pair(sampleChunk, 0));
+ return sampleChunk;
+ } else {
+ map.put(positionUs, new Pair(currentChunk, currentOffset));
+ return null;
+ }
}
/**
@@ -366,10 +418,10 @@ public class BufferManager {
* @throws IOException
*/
public void loadTrackFromStorage(String trackId, SamplePool samplePool) throws IOException {
- ArrayList<Long> keyPositions = mStorageManager.readIndexFile(trackId);
- long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0) : 0;
+ ArrayList<PositionHolder> keyPositions = mStorageManager.readIndexFile(trackId);
+ long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0).positionUs : 0;
- SortedMap<Long, SampleChunk> map = mChunkMap.get(trackId);
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(trackId);
if (map == null) {
map = new TreeMap<>();
mChunkMap.put(trackId, map);
@@ -377,11 +429,15 @@ public class BufferManager {
mPendingDelete.init(trackId);
}
SampleChunk chunk = null;
- for (long positionUs: keyPositions) {
- chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool,
- mStorageManager.getBufferDir(), getFileName(trackId, positionUs), positionUs,
- mChunkCallback, chunk);
- map.put(positionUs, chunk);
+ long basePositionUs = -1;
+ for (PositionHolder position: keyPositions) {
+ if (position.basePositionUs != basePositionUs) {
+ chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool,
+ mStorageManager.getBufferDir(), getFileName(trackId, position.positionUs),
+ position.positionUs, mChunkCallback, chunk);
+ basePositionUs = position.basePositionUs;
+ }
+ map.put(position.positionUs, new Pair(chunk, position.offset));
}
}
@@ -392,19 +448,19 @@ public class BufferManager {
* @param positionUs the position.
* @return returns the found {@link SampleChunk}.
*/
- public SampleChunk getReadFile(String id, long positionUs) {
- SortedMap<Long, SampleChunk> map = mChunkMap.get(id);
+ public Pair<SampleChunk, Integer> getReadFile(String id, long positionUs) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id);
if (map == null) {
return null;
}
- SampleChunk sampleChunk;
- SortedMap<Long, SampleChunk> headMap = map.headMap(positionUs + 1);
+ Pair<SampleChunk, Integer> ret;
+ SortedMap<Long, Pair<SampleChunk, Integer>> headMap = map.headMap(positionUs + 1);
if (!headMap.isEmpty()) {
- sampleChunk = headMap.get(headMap.lastKey());
+ ret = headMap.get(headMap.lastKey());
} else {
- sampleChunk = map.get(map.firstKey());
+ ret = map.get(map.firstKey());
}
- return sampleChunk;
+ return ret;
}
/**
@@ -439,15 +495,16 @@ public class BufferManager {
// Since chunks are persistent, we cannot evict chunks.
return false;
}
- SortedMap<Long, SampleChunk> earliestChunkMap = null;
+ SortedMap<Long, Pair<SampleChunk, Integer>> earliestChunkMap = null;
SampleChunk earliestChunk = null;
String earliestChunkId = null;
- for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) {
- SortedMap<Long, SampleChunk> map = entry.getValue();
+ for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
+ mChunkMap.entrySet()) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue();
if (map.isEmpty()) {
continue;
}
- SampleChunk chunk = map.get(map.firstKey());
+ SampleChunk chunk = map.get(map.firstKey()).first;
if (earliestChunk == null
|| chunk.getCreatedTimeMs() < earliestChunk.getCreatedTimeMs()) {
earliestChunkMap = map;
@@ -473,8 +530,9 @@ public class BufferManager {
}
pendingDelete = mPendingDelete.getSize();
}
- for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) {
- SortedMap<Long, SampleChunk> map = entry.getValue();
+ for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
+ mChunkMap.entrySet()) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue();
if (map.isEmpty()) {
continue;
}
@@ -489,70 +547,74 @@ public class BufferManager {
* @return returns all track information which is found by {@link BufferManager.StorageManager}.
* @throws IOException
*/
- public ArrayList<Pair<String, MediaFormat>> readTrackInfoFiles() throws IOException {
- ArrayList<Pair<String, MediaFormat>> trackInfos = new ArrayList<>();
- try {
- trackInfos.add(mStorageManager.readTrackInfoFile(false));
- } catch (FileNotFoundException e) {
- // There can be a single track only recording. (eg. audio-only, video-only)
- // So the exception should not stop the read.
+ public List<TrackFormat> readTrackInfoFiles() throws IOException {
+ List<TrackFormat> trackFormatList = new ArrayList<>();
+ trackFormatList.addAll(mStorageManager.readTrackInfoFiles(false));
+ trackFormatList.addAll(mStorageManager.readTrackInfoFiles(true));
+ if (trackFormatList.isEmpty()) {
+ throw new IOException("No track information to load");
}
- try {
- trackInfos.add(mStorageManager.readTrackInfoFile(true));
- } catch (FileNotFoundException e) {
- // See above catch block.
- }
- return trackInfos;
+ return trackFormatList;
}
/**
* Writes track information and index information for all tracks.
*
- * @param audio audio information.
- * @param video video information.
+ * @param audios list of audio track information
+ * @param videos list of audio track information
* @throws IOException
*/
- public void writeMetaFiles(Pair<String, MediaFormat> audio, Pair<String, MediaFormat> video)
+ public void writeMetaFiles(List<TrackFormat> audios, List<TrackFormat> videos)
throws IOException {
- if (audio != null) {
- mStorageManager.writeTrackInfoFile(audio.first, audio.second, true);
- SortedMap<Long, SampleChunk> map = mChunkMap.get(audio.first);
- if (map == null) {
- throw new IOException("Audio track index missing");
+ if (audios.isEmpty() && videos.isEmpty()) {
+ throw new IOException("No track information to save");
+ }
+ if (!audios.isEmpty()) {
+ mStorageManager.writeTrackInfoFiles(audios, true);
+ for (TrackFormat trackFormat : audios) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map =
+ mChunkMap.get(trackFormat.trackId);
+ if (map == null) {
+ throw new IOException("Audio track index missing");
+ }
+ mStorageManager.writeIndexFile(trackFormat.trackId, map);
}
- mStorageManager.writeIndexFile(audio.first, map);
}
- if (video != null) {
- mStorageManager.writeTrackInfoFile(video.first, video.second, false);
- SortedMap<Long, SampleChunk> map = mChunkMap.get(video.first);
- if (map == null) {
- throw new IOException("Video track index missing");
+ if (!videos.isEmpty()) {
+ mStorageManager.writeTrackInfoFiles(videos, false);
+ for (TrackFormat trackFormat : videos) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map =
+ mChunkMap.get(trackFormat.trackId);
+ if (map == null) {
+ throw new IOException("Video track index missing");
+ }
+ mStorageManager.writeIndexFile(trackFormat.trackId, map);
}
- mStorageManager.writeIndexFile(video.first, map);
}
}
/**
- * Marks it is closed and it is not used anymore.
- */
- public void close() {
- // Clean-up may happen after this is called.
- mClosed = true;
- }
-
- /**
* Releases all the resources.
*/
public void release() {
- mPendingDelete.release();
- for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) {
- for (SampleChunk chunk : entry.getValue().values()) {
- SampleChunk.IoState.release(chunk, !mStorageManager.isPersistent());
+ try {
+ mPendingDelete.release();
+ for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
+ mChunkMap.entrySet()) {
+ SampleChunk toRelease = null;
+ for (Pair<SampleChunk, Integer> positions : entry.getValue().values()) {
+ if (toRelease != positions.first) {
+ toRelease = positions.first;
+ SampleChunk.IoState.release(toRelease, !mStorageManager.isPersistent());
+ }
+ }
}
- }
- mChunkMap.clear();
- if (mClosed) {
- clearBuffer(!mStorageManager.isPersistent());
+ mChunkMap.clear();
+ } catch (ConcurrentModificationException | NullPointerException e) {
+ // TODO: remove this after it it confirmed that race condition issues are resolved.
+ // b/32492258, b/32373376
+ SoftPreconditions.checkState(false, "Exception on BufferManager#release: ",
+ e.toString());
}
}
@@ -611,20 +673,6 @@ public class BufferManager {
}
/**
- * Marks {@link BufferManager} object disabled to prevent it from the future use.
- */
- public void disable() {
- mDisabled = true;
- }
-
- /**
- * Returns if {@link BufferManager} object is disabled.
- */
- public boolean isDisabled() {
- return mDisabled;
- }
-
- /**
* Returns if {@link BufferManager} has checked the write speed,
* which is suitable for Trickplay.
*/
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
index 6a0502a7..6a09016c 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
@@ -17,8 +17,12 @@
package com.android.tv.tuner.exoplayer.buffer;
import android.media.MediaFormat;
+import android.util.Log;
import android.util.Pair;
+import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.google.protobuf.nano.MessageNano;
+
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
@@ -28,18 +32,25 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
import java.util.SortedMap;
/**
* Manages DVR storage.
*/
public class DvrStorageManager implements BufferManager.StorageManager {
+ private static final String TAG = "DvrStorageManager";
// TODO: make serializable classes and use protobuf after internal data structure is finalized.
private static final String KEY_PIXEL_WIDTH_HEIGHT_RATIO =
"com.google.android.videos.pixelWidthHeightRatio";
+ private static final String META_FILE_TYPE_AUDIO = "audio";
+ private static final String META_FILE_TYPE_VIDEO = "video";
+ private static final String META_FILE_TYPE_CAPTION = "caption";
private static final String META_FILE_SUFFIX = ".meta";
private static final String IDX_FILE_SUFFIX = ".idx";
+ private static final String IDX_FILE_SUFFIX_V2 = IDX_FILE_SUFFIX + "2";
// Size of minimum reserved storage buffer which will be used to save meta files
// and index files after actual recording finished.
@@ -59,18 +70,6 @@ public class DvrStorageManager implements BufferManager.StorageManager {
}
@Override
- public void clearStorage() {
- if (mIsRecording) {
- File[] files = mBufferDir.listFiles();
- if (files != null && files.length > 0) {
- for (File file : files) {
- file.delete();
- }
- }
- }
- }
-
- @Override
public File getBufferDir() {
return mBufferDir;
}
@@ -132,6 +131,17 @@ public class DvrStorageManager implements BufferManager.StorageManager {
}
}
+ private void readFormatStringOptional(DataInputStream in, MediaFormat format, String key) {
+ try {
+ String str = readString(in);
+ if (str != null) {
+ format.setString(key, str);
+ }
+ } catch (IOException e) {
+ // Since we are reading optional field, ignore the exception.
+ }
+ }
+
private ByteBuffer readByteBuffer(DataInputStream in) throws IOException {
int len = in.readInt();
if (len <= 0) {
@@ -155,39 +165,104 @@ public class DvrStorageManager implements BufferManager.StorageManager {
}
@Override
- public Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException {
- File file = new File(getBufferDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX);
- try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
- String name = readString(in);
- MediaFormat format = new MediaFormat();
- readFormatString(in, format, MediaFormat.KEY_MIME);
- readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE);
- readFormatInt(in, format, MediaFormat.KEY_WIDTH);
- readFormatInt(in, format, MediaFormat.KEY_HEIGHT);
- readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT);
- readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE);
- readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
- for (int i = 0; i < 3; ++i) {
- readFormatByteBuffer(in, format, "csd-" + i);
+ public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) {
+ List<BufferManager.TrackFormat> trackFormatList = new ArrayList<>();
+ int index = 0;
+ boolean trackNotFound = false;
+ do {
+ String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO)
+ + ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
+ String name = readString(in);
+ MediaFormat format = new MediaFormat();
+ readFormatString(in, format, MediaFormat.KEY_MIME);
+ readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE);
+ readFormatInt(in, format, MediaFormat.KEY_WIDTH);
+ readFormatInt(in, format, MediaFormat.KEY_HEIGHT);
+ readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT);
+ readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE);
+ readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
+ for (int i = 0; i < 3; ++i) {
+ readFormatByteBuffer(in, format, "csd-" + i);
+ }
+ readFormatLong(in, format, MediaFormat.KEY_DURATION);
+
+ // This is optional since language field is added later.
+ readFormatStringOptional(in, format, MediaFormat.KEY_LANGUAGE);
+ trackFormatList.add(new BufferManager.TrackFormat(name, format));
+ } catch (IOException e) {
+ trackNotFound = true;
}
- readFormatLong(in, format, MediaFormat.KEY_DURATION);
- return new Pair<>(name, format);
+ index++;
+ } while(!trackNotFound);
+ return trackFormatList;
+ }
+
+ /**
+ * Reads caption information from files.
+ *
+ * @return a list of {@link AtscCaptionTrack} objects which store caption information.
+ */
+ public List<AtscCaptionTrack> readCaptionInfoFiles() {
+ List<AtscCaptionTrack> tracks = new ArrayList<>();
+ int index = 0;
+ boolean trackNotFound = false;
+ do {
+ String fileName = META_FILE_TYPE_CAPTION +
+ ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
+ byte[] data = new byte[(int) file.length()];
+ in.read(data);
+ tracks.add(AtscCaptionTrack.parseFrom(data));
+ } catch (IOException e) {
+ trackNotFound = true;
+ }
+ index++;
+ } while(!trackNotFound);
+ return tracks;
+ }
+
+ private ArrayList<BufferManager.PositionHolder> readOldIndexFile(File indexFile)
+ throws IOException {
+ ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>();
+ try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) {
+ long count = in.readLong();
+ for (long i = 0; i < count; ++i) {
+ long positionUs = in.readLong();
+ indices.add(new BufferManager.PositionHolder(positionUs, positionUs, 0));
+ }
+ return indices;
}
}
- @Override
- public ArrayList<Long> readIndexFile(String trackId) throws IOException {
- ArrayList<Long> indices = new ArrayList<>();
- File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX);
- try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
+ private ArrayList<BufferManager.PositionHolder> readNewIndexFile(File indexFile)
+ throws IOException {
+ ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>();
+ try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) {
long count = in.readLong();
for (long i = 0; i < count; ++i) {
- indices.add(in.readLong());
+ long positionUs = in.readLong();
+ long basePositionUs = in.readLong();
+ int offset = in.readInt();
+ indices.add(new BufferManager.PositionHolder(positionUs, basePositionUs, offset));
}
return indices;
}
}
+ @Override
+ public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId)
+ throws IOException {
+ File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX_V2);
+ if (file.exists()) {
+ return readNewIndexFile(file);
+ } else {
+ return readOldIndexFile(new File(getBufferDir(),trackId + IDX_FILE_SUFFIX));
+ }
+ }
+
private void writeFormatInt(DataOutputStream out, MediaFormat format, String key)
throws IOException {
if (format.containsKey(key)) {
@@ -254,33 +329,63 @@ public class DvrStorageManager implements BufferManager.StorageManager {
}
@Override
- public void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio)
+ public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio)
throws IOException {
- File file = new File(getBufferDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX);
- try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
- writeString(out, trackId);
- writeFormatString(out, format, MediaFormat.KEY_MIME);
- writeFormatInt(out, format, MediaFormat.KEY_MAX_INPUT_SIZE);
- writeFormatInt(out, format, MediaFormat.KEY_WIDTH);
- writeFormatInt(out, format, MediaFormat.KEY_HEIGHT);
- writeFormatInt(out, format, MediaFormat.KEY_CHANNEL_COUNT);
- writeFormatInt(out, format, MediaFormat.KEY_SAMPLE_RATE);
- writeFormatFloat(out, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
- for (int i = 0; i < 3; ++i) {
- writeFormatByteBuffer(out, format, "csd-" + i);
+ for (int i = 0; i < formatList.size() ; ++i) {
+ BufferManager.TrackFormat trackFormat = formatList.get(i);
+ String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO)
+ + ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
+ writeString(out, trackFormat.trackId);
+ writeFormatString(out, trackFormat.format, MediaFormat.KEY_MIME);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_MAX_INPUT_SIZE);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_WIDTH);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_HEIGHT);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_CHANNEL_COUNT);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_SAMPLE_RATE);
+ writeFormatFloat(out, trackFormat.format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
+ for (int j = 0; j < 3; ++j) {
+ writeFormatByteBuffer(out, trackFormat.format, "csd-" + j);
+ }
+ writeFormatLong(out, trackFormat.format, MediaFormat.KEY_DURATION);
+ writeFormatString(out, trackFormat.format, MediaFormat.KEY_LANGUAGE);
+ }
+ }
+ }
+
+ /**
+ * Writes caption information to files.
+ *
+ * @param tracks a list of {@link AtscCaptionTrack} objects which store caption information.
+ */
+ public void writeCaptionInfoFiles(List<AtscCaptionTrack> tracks) {
+ if (tracks == null || tracks.isEmpty()) {
+ return;
+ }
+ for (int i = 0; i < tracks.size(); i++) {
+ AtscCaptionTrack track = tracks.get(i);
+ String fileName = META_FILE_TYPE_CAPTION +
+ ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
+ out.write(MessageNano.toByteArray(track));
+ } catch (Exception e) {
+ Log.e(TAG, "Fail to write caption info to files", e);
}
- writeFormatLong(out, format, MediaFormat.KEY_DURATION);
}
}
@Override
- public void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index)
+ public void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)
throws IOException {
- File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX);
+ File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX_V2);
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(indexFile))) {
out.writeLong(index.size());
- for (Long key : index.keySet()) {
- out.writeLong(key);
+ for (Map.Entry<Long, Pair<SampleChunk, Integer>> entry : index.entrySet()) {
+ out.writeLong(entry.getKey());
+ out.writeLong(entry.getValue().first.getStartPositionUs());
+ out.writeInt(entry.getValue().second);
}
}
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
index 4869b49f..af0c3f0d 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
@@ -66,9 +66,14 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
public static final int BUFFER_REASON_RECORDING = 2;
/**
- * The duration of a chunk of samples, {@link SampleChunk}.
+ * The minimum duration to support seek in Trickplay.
*/
- static final long CHUNK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500);
+ static final long MIN_SEEK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500);
+
+ /**
+ * The duration of a {@link SampleChunk} for recordings.
+ */
+ static final long RECORDING_CHUNK_DURATION_US = MIN_SEEK_DURATION_US * 1200; // 10 minutes
private static final long BUFFER_WRITE_TIMEOUT_MS = 10 * 1000; // 10 seconds
private static final long BUFFER_NEEDED_US =
1000L * Math.max(MpegTsPlayer.MIN_BUFFER_MS, MpegTsPlayer.MIN_REBUFFER_MS);
@@ -79,7 +84,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
private int mTrackCount;
private boolean[] mTrackSelected;
- private List<String> mIds;
private List<SampleQueue> mReadSampleQueues;
private final SamplePool mSamplePool = new SamplePool();
private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US;
@@ -130,7 +134,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
if (mTrackCount <= 0) {
throw new IOException("No tracks to initialize");
}
- mIds = ids;
mTrackSelected = new boolean[mTrackCount];
mReadSampleQueues = new ArrayList<>();
mSampleChunkIoHelper = new SampleChunkIoHelper(ids, mediaFormats, mBufferReason,
@@ -139,6 +142,9 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
mReadSampleQueues.add(i, new SampleQueue(mSamplePool));
}
mSampleChunkIoHelper.init();
+ for (int i = 0; i < mTrackCount; ++i) {
+ mBufferManager.registerChunkEvictedListener(ids.get(i), RecordingSampleBuffer.this);
+ }
}
@Override
@@ -146,8 +152,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
if (!mTrackSelected[index]) {
mTrackSelected[index] = true;
mReadSampleQueues.get(index).clear();
- mBufferManager.registerChunkEvictedListener(mIds.get(index),
- RecordingSampleBuffer.this);
mSampleChunkIoHelper.openRead(index, mCurrentPlaybackPositionUs);
}
}
@@ -157,7 +161,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
if (mTrackSelected[index]) {
mTrackSelected[index] = false;
mReadSampleQueues.get(index).clear();
- mBufferManager.unregisterChunkEvictedListener(mIds.get(index));
+ mSampleChunkIoHelper.closeRead(index);
}
}
@@ -193,7 +197,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
}
// Disables buffering samples afterwards, and notifies the disk speed is slow.
Log.w(TAG, "Disk is too slow for trickplay");
- mBufferManager.disable();
mBufferListener.onDiskTooSlow();
}
@@ -205,7 +208,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
private boolean maybeReadSample(SampleQueue queue, int index) {
if (queue.getLastQueuedPositionUs() != null
&& queue.getLastQueuedPositionUs() > mCurrentPlaybackPositionUs + BUFFER_NEEDED_US
- && queue.isDurationGreaterThan(CHUNK_DURATION_US)) {
+ && queue.isDurationGreaterThan(MIN_SEEK_DURATION_US)) {
// The speed of queuing samples can be higher than the playback speed.
// If the duration of the samples in the queue is not limited,
// samples can be accumulated and there can be out-of-memory issues.
@@ -300,7 +303,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
public void onChunkEvicted(String id, long createdTimeMs) {
if (mBufferListener != null) {
mBufferListener.onBufferStartTimeChanged(
- createdTimeMs + TimeUnit.MICROSECONDS.toMillis(CHUNK_DURATION_US));
+ createdTimeMs + TimeUnit.MICROSECONDS.toMillis(MIN_SEEK_DURATION_US));
}
}
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java
index 552caaef..04b5a071 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java
@@ -78,7 +78,6 @@ public class SampleChunk {
/**
* A class for SampleChunk creation.
*/
- @VisibleForTesting
public static class SampleChunkCreator {
/**
@@ -151,18 +150,23 @@ public class SampleChunk {
mCurrentOffset = 0;
}
+ private void reset(SampleChunk chunk, long offset) {
+ mChunk = chunk;
+ mCurrentOffset = offset;
+ }
+
/**
* Prepares for read I/O operation from a new SampleChunk.
*
* @param chunk the new SampleChunk to read from
* @throws IOException
*/
- void openRead(SampleChunk chunk) throws IOException {
+ void openRead(SampleChunk chunk, long offset) throws IOException {
if (mChunk != null) {
mChunk.closeRead();
}
chunk.openRead();
- reset(chunk);
+ reset(chunk, offset);
}
/**
@@ -241,6 +245,20 @@ public class SampleChunk {
}
/**
+ * Returns the current SampleChunk for subsequent I/O operation.
+ */
+ SampleChunk getChunk() {
+ return mChunk;
+ }
+
+ /**
+ * Returns the current offset of the current SampleChunk for subsequent I/O operation.
+ */
+ long getOffset() {
+ return mCurrentOffset;
+ }
+
+ /**
* Releases SampleChunk. the SampleChunk will not be used anymore.
*
* @param chunk to release
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
index 37ae4022..ca97a91a 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
@@ -21,6 +21,7 @@ import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
+import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
@@ -31,7 +32,9 @@ import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer.BufferReason;
import java.io.IOException;
+import java.util.LinkedList;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
@@ -46,11 +49,13 @@ public class SampleChunkIoHelper implements Handler.Callback {
private static final int MSG_OPEN_READ = 1;
private static final int MSG_OPEN_WRITE = 2;
- private static final int MSG_CLOSE_WRITE = 3;
- private static final int MSG_READ = 4;
- private static final int MSG_WRITE = 5;
- private static final int MSG_RELEASE = 6;
+ private static final int MSG_CLOSE_READ = 3;
+ private static final int MSG_CLOSE_WRITE = 4;
+ private static final int MSG_READ = 5;
+ private static final int MSG_WRITE = 6;
+ private static final int MSG_RELEASE = 7;
+ private final long mSampleChunkDurationUs;
private final int mTrackCount;
private final List<String> mIds;
private final List<MediaFormat> mMediaFormats;
@@ -62,9 +67,11 @@ public class SampleChunkIoHelper implements Handler.Callback {
private Handler mIoHandler;
private final ConcurrentLinkedQueue<SampleHolder> mReadSampleBuffers[];
private final ConcurrentLinkedQueue<SampleHolder> mHandlerReadSampleBuffers[];
- private final long[] mWriteEndPositionUs;
+ private final long[] mWriteIndexEndPositionUs;
+ private final long[] mWriteChunkEndPositionUs;
private final SampleChunk.IoState[] mReadIoStates;
private final SampleChunk.IoState[] mWriteIoStates;
+ private final Set<Integer> mSelectedTracks = new ArraySet<>();
private long mBufferDurationUs = 0;
private boolean mWriteEnded;
private boolean mErrorNotified;
@@ -129,11 +136,20 @@ public class SampleChunkIoHelper implements Handler.Callback {
mReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount];
mHandlerReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount];
- mWriteEndPositionUs = new long[mTrackCount];
+ mWriteIndexEndPositionUs = new long[mTrackCount];
+ mWriteChunkEndPositionUs = new long[mTrackCount];
mReadIoStates = new SampleChunk.IoState[mTrackCount];
mWriteIoStates = new SampleChunk.IoState[mTrackCount];
+
+ // Small chunk duration for live playback will give more fine grained storage usage
+ // and eviction handling for trickplay.
+ mSampleChunkDurationUs =
+ bufferReason == RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK ?
+ RecordingSampleBuffer.MIN_SEEK_DURATION_US :
+ RecordingSampleBuffer.RECORDING_CHUNK_DURATION_US;
for (int i = 0; i < mTrackCount; ++i) {
- mWriteEndPositionUs[i] = RecordingSampleBuffer.CHUNK_DURATION_US;
+ mWriteIndexEndPositionUs[i] = RecordingSampleBuffer.MIN_SEEK_DURATION_US;
+ mWriteChunkEndPositionUs[i] = mSampleChunkDurationUs;
mReadIoStates[i] = new SampleChunk.IoState();
mWriteIoStates[i] = new SampleChunk.IoState();
}
@@ -204,6 +220,15 @@ public class SampleChunkIoHelper implements Handler.Callback {
}
/**
+ * Closes read from the specified track.
+ *
+ * @param index track index
+ */
+ public void closeRead(int index) {
+ mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_CLOSE_READ, index));
+ }
+
+ /**
* Notifies writes are finished.
*/
public void closeWrite() {
@@ -229,21 +254,19 @@ public class SampleChunkIoHelper implements Handler.Callback {
try {
if (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING && mTrackCount > 0) {
// Saves meta information for recording.
- Pair<String, android.media.MediaFormat> audio = null, video = null;
+ List<BufferManager.TrackFormat> audios = new LinkedList<>();
+ List<BufferManager.TrackFormat> videos = new LinkedList<>();
for (int i = 0; i < mTrackCount; ++i) {
android.media.MediaFormat format =
mMediaFormats.get(i).getFrameworkMediaFormatV16();
format.setLong(android.media.MediaFormat.KEY_DURATION, mBufferDurationUs);
- if (audio == null && MimeTypes.isAudio(mMediaFormats.get(i).mimeType)) {
- audio = new Pair<>(mIds.get(i), format);
- } else if (video == null && MimeTypes.isVideo(mMediaFormats.get(i).mimeType)) {
- video = new Pair<>(mIds.get(i), format);
- }
- if (audio != null && video != null) {
- break;
+ if (MimeTypes.isAudio(mMediaFormats.get(i).mimeType)) {
+ audios.add(new BufferManager.TrackFormat(mIds.get(i), format));
+ } else if (MimeTypes.isVideo(mMediaFormats.get(i).mimeType)) {
+ videos.add(new BufferManager.TrackFormat(mIds.get(i), format));
}
}
- mBufferManager.writeMetaFiles(audio, video);
+ mBufferManager.writeMetaFiles(audios, videos);
}
} finally {
mBufferManager.release();
@@ -265,6 +288,9 @@ public class SampleChunkIoHelper implements Handler.Callback {
case MSG_OPEN_WRITE:
doOpenWrite((int) message.obj);
return true;
+ case MSG_CLOSE_READ:
+ doCloseRead((int) message.obj);
+ return true;
case MSG_CLOSE_WRITE:
doCloseWrite();
return true;
@@ -291,14 +317,16 @@ public class SampleChunkIoHelper implements Handler.Callback {
private void doOpenRead(IoParams params) throws IOException {
int index = params.index;
mIoHandler.removeMessages(MSG_READ, index);
- SampleChunk chunk = mBufferManager.getReadFile(mIds.get(index), params.positionUs);
- if (chunk == null) {
+ Pair<SampleChunk, Integer> readPosition =
+ mBufferManager.getReadFile(mIds.get(index), params.positionUs);
+ if (readPosition == null) {
String errorMessage = "Chunk ID:" + mIds.get(index) + " pos:" + params.positionUs
+ "is not found";
- SoftPreconditions.checkNotNull(chunk, TAG, errorMessage);
+ SoftPreconditions.checkNotNull(readPosition, TAG, errorMessage);
throw new IOException(errorMessage);
}
- mReadIoStates[index].openRead(chunk);
+ mSelectedTracks.add(index);
+ mReadIoStates[index].openRead(readPosition.first, (long) readPosition.second);
if (mHandlerReadSampleBuffers[index] != null) {
SampleHolder sample;
while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) {
@@ -310,10 +338,22 @@ public class SampleChunkIoHelper implements Handler.Callback {
}
private void doOpenWrite(int index) throws IOException {
- SampleChunk chunk = mBufferManager.createNewWriteFile(mIds.get(index), 0, mSamplePool);
+ SampleChunk chunk = mBufferManager.createNewWriteFileIfNeeded(mIds.get(index), 0,
+ mSamplePool, null, 0);
mWriteIoStates[index].openWrite(chunk);
}
+ private void doCloseRead(int index) {
+ mSelectedTracks.remove(index);
+ if (mHandlerReadSampleBuffers[index] != null) {
+ SampleHolder sample;
+ while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) {
+ mSamplePool.releaseSample(sample);
+ }
+ }
+ mIoHandler.removeMessages(MSG_READ, index);
+ }
+
private void doRead(int index) throws IOException {
mIoHandler.removeMessages(MSG_READ, index);
if (mHandlerReadSampleBuffers[index].size() >= MAX_READ_BUFFER_SAMPLES) {
@@ -357,13 +397,21 @@ public class SampleChunkIoHelper implements Handler.Callback {
if (sample.timeUs > mBufferDurationUs) {
mBufferDurationUs = sample.timeUs;
}
-
- if (sample.timeUs >= mWriteEndPositionUs[index]) {
- nextChunk = mBufferManager.createNewWriteFile(mIds.get(index),
- mWriteEndPositionUs[index], mSamplePool);
- mWriteEndPositionUs[index] =
- ((sample.timeUs / RecordingSampleBuffer.CHUNK_DURATION_US) + 1) *
- RecordingSampleBuffer.CHUNK_DURATION_US;
+ if (sample.timeUs >= mWriteIndexEndPositionUs[index]) {
+ SampleChunk currentChunk = sample.timeUs >= mWriteChunkEndPositionUs[index] ?
+ null : mWriteIoStates[params.index].getChunk();
+ int currentOffset = (int) mWriteIoStates[params.index].getOffset();
+ nextChunk = mBufferManager.createNewWriteFileIfNeeded(
+ mIds.get(index), mWriteIndexEndPositionUs[index], mSamplePool,
+ currentChunk, currentOffset);
+ mWriteIndexEndPositionUs[index] =
+ ((sample.timeUs / RecordingSampleBuffer.MIN_SEEK_DURATION_US) + 1) *
+ RecordingSampleBuffer.MIN_SEEK_DURATION_US;
+ if (nextChunk != null) {
+ mWriteChunkEndPositionUs[index] =
+ ((sample.timeUs / mSampleChunkDurationUs) + 1)
+ * mSampleChunkDurationUs;
+ }
}
}
mWriteIoStates[params.index].write(params.sample, nextChunk);
@@ -391,15 +439,22 @@ public class SampleChunkIoHelper implements Handler.Callback {
mIoHandler.removeCallbacksAndMessages(null);
mFinished = true;
conditionVariable.open();
+ mSelectedTracks.clear();
}
private void releaseEvictedChunks() {
- if (mBufferReason != RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK) {
+ if (mBufferReason != RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK
+ || mSelectedTracks.isEmpty()) {
return;
}
+ long currentStartPositionUs = Long.MAX_VALUE;
+ for (int trackIndex : mSelectedTracks) {
+ currentStartPositionUs = Math.min(currentStartPositionUs,
+ mReadIoStates[trackIndex].getStartPositionUs());
+ }
for (int i = 0; i < mTrackCount; ++i) {
long evictEndPositionUs = Math.min(mBufferManager.getStartPositionUs(mIds.get(i)),
- mReadIoStates[i].getStartPositionUs());
+ currentStartPositionUs);
mBufferManager.evictChunks(mIds.get(i), evictEndPositionUs);
}
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java
index 7b098f40..75eac5a2 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java
@@ -43,6 +43,7 @@ public class SampleQueue {
if (sampleFromQueue == null) {
return SampleSource.NOTHING_READ;
}
+ sample.ensureSpaceForWrite(sampleFromQueue.size);
sample.size = sampleFromQueue.size;
sample.flags = sampleFromQueue.flags;
sample.timeUs = sampleFromQueue.timeUs;
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
index 40c4ef95..159fde18 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
@@ -19,18 +19,18 @@ package com.android.tv.tuner.exoplayer.buffer;
import android.os.ConditionVariable;
import android.support.annotation.NonNull;
+
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.tvinput.PlaybackBufferListener;
import com.android.tv.tuner.exoplayer.SampleExtractor;
import java.io.IOException;
import java.util.List;
-import junit.framework.Assert;
-
/**
* Handles I/O for {@link SampleExtractor} when
* physical storage based buffer is not used. Trickplay is disabled.
@@ -115,8 +115,8 @@ public class SimpleSampleBuffer implements BufferManager.SampleBuffer {
@Override
public synchronized int readSample(int track, SampleHolder sampleHolder) {
SampleQueue queue = mPlayingSampleQueues[track];
- Assert.assertNotNull(queue);
- int result = queue.dequeueSample(sampleHolder);
+ SoftPreconditions.checkNotNull(queue);
+ int result = queue == null ? SampleSource.NOTHING_READ : queue.dequeueSample(sampleHolder);
if (result != SampleSource.SAMPLE_READ && reachedEos()) {
return SampleSource.END_OF_STREAM;
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
index 258a5cd0..9fe921b8 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
@@ -17,20 +17,23 @@
package com.android.tv.tuner.exoplayer.buffer;
import android.content.Context;
-import android.media.MediaFormat;
import android.os.AsyncTask;
-import android.os.Looper;
import android.provider.Settings;
+import android.support.annotation.NonNull;
import android.util.Pair;
+import com.android.tv.common.SoftPreconditions;
+
import java.io.File;
import java.util.ArrayList;
+import java.util.List;
import java.util.SortedMap;
/**
* Manages Trickplay storage.
*/
public class TrickplayStorageManager implements BufferManager.StorageManager {
+ // TODO: Support multi-sessions.
private static final String BUFFER_DIR = "timeshift";
// Copied from android.provider.Settings.Global (hidden fields)
@@ -43,53 +46,68 @@ public class TrickplayStorageManager implements BufferManager.StorageManager {
private static final int DEFAULT_THRESHOLD_PERCENTAGE = 10;
private static final long DEFAULT_THRESHOLD_MAX_BYTES = 500L * 1024 * 1024;
- private final File mBufferDir;
+ private static AsyncTask<Void, Void, Void> sLastCacheCleanUpTask;
+ private static File sBufferDir;
+ private static long sStorageBufferBytes;
+
private final long mMaxBufferSize;
- private final long mStorageBufferBytes;
- private static long getStorageBufferBytes(Context context, File path) {
+ private static void initParamsIfNeeded(Context context, @NonNull File path) {
+ // TODO: Support multi-sessions.
+ SoftPreconditions.checkState(
+ sBufferDir == null || sBufferDir.equals(path));
+ if (path.equals(sBufferDir)) {
+ return;
+ }
+ sBufferDir = path;
long lowPercentage = Settings.Global.getInt(context.getContentResolver(),
SYS_STORAGE_THRESHOLD_PERCENTAGE, DEFAULT_THRESHOLD_PERCENTAGE);
- long lowBytes = path.getTotalSpace() * lowPercentage / 100;
+ long lowPercentageToBytes = path.getTotalSpace() * lowPercentage / 100;
long maxLowBytes = Settings.Global.getLong(context.getContentResolver(),
SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES);
- return Math.min(lowBytes, maxLowBytes);
+ sStorageBufferBytes = Math.min(lowPercentageToBytes, maxLowBytes);
}
- public TrickplayStorageManager(Context context, File baseDir, long maxBufferSize) {
- mBufferDir = new File(baseDir, BUFFER_DIR);
- mBufferDir.mkdirs();
+ public TrickplayStorageManager(Context context, @NonNull File baseDir, long maxBufferSize) {
+ initParamsIfNeeded(context, new File(baseDir, BUFFER_DIR));
+ sBufferDir.mkdirs();
mMaxBufferSize = maxBufferSize;
clearStorage();
- mStorageBufferBytes = getStorageBufferBytes(context, mBufferDir);
}
- @Override
- public void clearStorage() {
- File files[] = mBufferDir.listFiles();
- if (files == null || files.length == 0) {
- return;
+ private void clearStorage() {
+ long now = System.currentTimeMillis();
+ if (sLastCacheCleanUpTask != null) {
+ sLastCacheCleanUpTask.cancel(true);
}
- if (Looper.myLooper() == Looper.getMainLooper()) {
- new AsyncTask<Void, Void, Void>() {
- @Override
- protected Void doInBackground(Void... params) {
- for (File file : files) {
+ sLastCacheCleanUpTask = new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (isCancelled()) {
+ return null;
+ }
+ File files[] = sBufferDir.listFiles();
+ if (files == null || files.length == 0) {
+ return null;
+ }
+ for (File file : files) {
+ if (isCancelled()) {
+ break;
+ }
+ long lastModified = file.lastModified();
+ if (lastModified != 0 && lastModified < now) {
file.delete();
}
- return null;
}
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- } else {
- for (File file : files) {
- file.delete();
+ return null;
}
- }
+ };
+ sLastCacheCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@Override
public File getBufferDir() {
- return mBufferDir;
+ return sBufferDir;
}
@Override
@@ -104,25 +122,26 @@ public class TrickplayStorageManager implements BufferManager.StorageManager {
@Override
public boolean hasEnoughBuffer(long pendingDelete) {
- return mBufferDir.getUsableSpace() + pendingDelete >= mStorageBufferBytes;
+ return sBufferDir.getUsableSpace() + pendingDelete >= sStorageBufferBytes;
}
@Override
- public Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) {
+ public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) {
return null;
}
@Override
- public ArrayList<Long> readIndexFile(String trackId) {
+ public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId) {
return null;
}
@Override
- public void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio) {
+ public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio) {
}
@Override
- public void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index) {
+ public void writeIndexFile(String trackName,
+ SortedMap<Long, Pair<SampleChunk, Integer>> index) {
}
}