• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.tuner.exoplayer.buffer;
18 
19 import android.os.ConditionVariable;
20 import android.support.annotation.IntDef;
21 import android.support.annotation.NonNull;
22 import android.util.Log;
23 
24 import com.google.android.exoplayer.C;
25 import com.google.android.exoplayer.MediaFormat;
26 import com.google.android.exoplayer.SampleHolder;
27 import com.google.android.exoplayer.SampleSource;
28 import com.google.android.exoplayer.util.Assertions;
29 import com.android.tv.tuner.exoplayer.MpegTsPlayer;
30 import com.android.tv.tuner.tvinput.PlaybackBufferListener;
31 import com.android.tv.tuner.exoplayer.SampleExtractor;
32 
33 import java.io.IOException;
34 import java.lang.annotation.Retention;
35 import java.lang.annotation.RetentionPolicy;
36 import java.util.ArrayList;
37 import java.util.List;
38 import java.util.concurrent.TimeUnit;
39 
40 /**
41  * Handles I/O between {@link SampleExtractor} and
42  * {@link BufferManager}.Reads & writes samples from/to {@link SampleChunk} which is backed
43  * by physical storage.
44  */
45 public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
46         BufferManager.ChunkEvictedListener {
47     private static final String TAG = "RecordingSampleBuffer";
48 
49     @IntDef({BUFFER_REASON_LIVE_PLAYBACK, BUFFER_REASON_RECORDED_PLAYBACK, BUFFER_REASON_RECORDING})
50     @Retention(RetentionPolicy.SOURCE)
51     public @interface BufferReason {}
52 
53     /**
54      * A buffer reason for live-stream playback.
55      */
56     public static final int BUFFER_REASON_LIVE_PLAYBACK = 0;
57 
58     /**
59      * A buffer reason for playback of a recorded program.
60      */
61     public static final int BUFFER_REASON_RECORDED_PLAYBACK = 1;
62 
63     /**
64      * A buffer reason for recording a program.
65      */
66     public static final int BUFFER_REASON_RECORDING = 2;
67 
68     /**
69      * The duration of a chunk of samples, {@link SampleChunk}.
70      */
71     static final long CHUNK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500);
72     private static final long BUFFER_WRITE_TIMEOUT_MS = 10 * 1000;  // 10 seconds
73     private static final long BUFFER_NEEDED_US =
74             1000L * Math.max(MpegTsPlayer.MIN_BUFFER_MS, MpegTsPlayer.MIN_REBUFFER_MS);
75 
76     private final BufferManager mBufferManager;
77     private final PlaybackBufferListener mBufferListener;
78     private final @BufferReason int mBufferReason;
79 
80     private int mTrackCount;
81     private boolean[] mTrackSelected;
82     private List<String> mIds;
83     private List<SampleQueue> mReadSampleQueues;
84     private final SamplePool mSamplePool = new SamplePool();
85     private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US;
86     private long mCurrentPlaybackPositionUs = 0;
87 
88     // An error in I/O thread of {@link SampleChunkIoHelper} will be notified.
89     private volatile boolean mError;
90 
91     // Eos was reached in I/O thread of {@link SampleChunkIoHelper}.
92     private volatile boolean mEos;
93     private SampleChunkIoHelper mSampleChunkIoHelper;
94     private final SampleChunkIoHelper.IoCallback mIoCallback =
95             new SampleChunkIoHelper.IoCallback() {
96         @Override
97         public void onIoReachedEos() {
98             mEos = true;
99         }
100 
101         @Override
102         public void onIoError() {
103             mError = true;
104         }
105     };
106 
107     /**
108      * Creates {@link BufferManager.SampleBuffer} with
109      * cached I/O backed by physical storage (e.g. trickplay,recording,recorded-playback).
110      *
111      * @param bufferManager the manager of {@link SampleChunk}
112      * @param bufferListener the listener for buffer I/O event
113      * @param enableTrickplay {@code true} when trickplay should be enabled
114      * @param bufferReason the reason for caching samples {@link RecordingSampleBuffer.BufferReason}
115      */
RecordingSampleBuffer(BufferManager bufferManager, PlaybackBufferListener bufferListener, boolean enableTrickplay, @BufferReason int bufferReason)116     public RecordingSampleBuffer(BufferManager bufferManager, PlaybackBufferListener bufferListener,
117             boolean enableTrickplay, @BufferReason int bufferReason) {
118         mBufferManager = bufferManager;
119         mBufferListener = bufferListener;
120         if (bufferListener != null) {
121             bufferListener.onBufferStateChanged(enableTrickplay);
122         }
123         mBufferReason = bufferReason;
124     }
125 
126     @Override
init(@onNull List<String> ids, @NonNull List<MediaFormat> mediaFormats)127     public void init(@NonNull List<String> ids, @NonNull List<MediaFormat> mediaFormats)
128             throws IOException {
129         mTrackCount = ids.size();
130         if (mTrackCount <= 0) {
131             throw new IOException("No tracks to initialize");
132         }
133         mIds = ids;
134         mTrackSelected = new boolean[mTrackCount];
135         mReadSampleQueues = new ArrayList<>();
136         mSampleChunkIoHelper = new SampleChunkIoHelper(ids, mediaFormats, mBufferReason,
137                 mBufferManager, mSamplePool, mIoCallback);
138         for (int i = 0; i < mTrackCount; ++i) {
139             mReadSampleQueues.add(i, new SampleQueue(mSamplePool));
140         }
141         mSampleChunkIoHelper.init();
142     }
143 
144     @Override
selectTrack(int index)145     public void selectTrack(int index) {
146         if (!mTrackSelected[index]) {
147             mTrackSelected[index] = true;
148             mReadSampleQueues.get(index).clear();
149             mBufferManager.registerChunkEvictedListener(mIds.get(index),
150                     RecordingSampleBuffer.this);
151             mSampleChunkIoHelper.openRead(index, mCurrentPlaybackPositionUs);
152         }
153     }
154 
155     @Override
deselectTrack(int index)156     public void deselectTrack(int index) {
157         if (mTrackSelected[index]) {
158             mTrackSelected[index] = false;
159             mReadSampleQueues.get(index).clear();
160             mBufferManager.unregisterChunkEvictedListener(mIds.get(index));
161         }
162     }
163 
164     @Override
writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable)165     public void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
166             throws IOException {
167         mSampleChunkIoHelper.writeSample(index, sample, conditionVariable);
168 
169         if (!conditionVariable.block(BUFFER_WRITE_TIMEOUT_MS)) {
170             Log.e(TAG, "Error: Serious delay on writing buffer");
171             conditionVariable.block();
172         }
173     }
174 
175     @Override
isWriteSpeedSlow(int sampleSize, long writeDurationNs)176     public boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs) {
177         if (mBufferReason == BUFFER_REASON_RECORDED_PLAYBACK) {
178             return false;
179         }
180         mBufferManager.addWriteStat(sampleSize, writeDurationNs);
181         return mBufferManager.isWriteSlow();
182     }
183 
184     @Override
handleWriteSpeedSlow()185     public void handleWriteSpeedSlow() throws IOException{
186         if (mBufferReason == BUFFER_REASON_RECORDING) {
187             // Recording does not need to stop because I/O speed is slow temporarily.
188             // If fixed size buffer of TsStreamer overflows, TsDataSource will reach EoS.
189             // Reaching EoS will stop recording eventually.
190             Log.w(TAG, "Disk I/O speed is slow for recording temporarily: "
191                     + mBufferManager.getWriteBandwidth() + "MBps");
192             return;
193         }
194         // Disables buffering samples afterwards, and notifies the disk speed is slow.
195         Log.w(TAG, "Disk is too slow for trickplay");
196         mBufferManager.disable();
197         mBufferListener.onDiskTooSlow();
198     }
199 
200     @Override
setEos()201     public void setEos() {
202         mSampleChunkIoHelper.closeWrite();
203     }
204 
maybeReadSample(SampleQueue queue, int index)205     private boolean maybeReadSample(SampleQueue queue, int index) {
206         if (queue.getLastQueuedPositionUs() != null
207                 && queue.getLastQueuedPositionUs() > mCurrentPlaybackPositionUs + BUFFER_NEEDED_US
208                 && queue.isDurationGreaterThan(CHUNK_DURATION_US)) {
209             // The speed of queuing samples can be higher than the playback speed.
210             // If the duration of the samples in the queue is not limited,
211             // samples can be accumulated and there can be out-of-memory issues.
212             // But, the throttling should provide enough samples for the player to
213             // finish the buffering state.
214             return false;
215         }
216         SampleHolder sample = mSampleChunkIoHelper.readSample(index);
217         if (sample != null) {
218             queue.queueSample(sample);
219             return true;
220         }
221         return false;
222     }
223 
224     @Override
readSample(int track, SampleHolder outSample)225     public int readSample(int track, SampleHolder outSample) {
226         Assertions.checkState(mTrackSelected[track]);
227         maybeReadSample(mReadSampleQueues.get(track), track);
228         int result = mReadSampleQueues.get(track).dequeueSample(outSample);
229         if ((result != SampleSource.SAMPLE_READ && mEos) || mError) {
230             return SampleSource.END_OF_STREAM;
231         }
232         return result;
233     }
234 
235     @Override
seekTo(long positionUs)236     public void seekTo(long positionUs) {
237         for (int i = 0; i < mTrackCount; ++i) {
238             if (mTrackSelected[i]) {
239                 mReadSampleQueues.get(i).clear();
240                 mSampleChunkIoHelper.openRead(i, positionUs);
241             }
242         }
243         mLastBufferedPositionUs = positionUs;
244     }
245 
246     @Override
getBufferedPositionUs()247     public long getBufferedPositionUs() {
248         Long result = null;
249         for (int i = 0; i < mTrackCount; ++i) {
250             if (!mTrackSelected[i]) {
251                 continue;
252             }
253             Long lastQueuedSamplePositionUs =
254                     mReadSampleQueues.get(i).getLastQueuedPositionUs();
255             if (lastQueuedSamplePositionUs == null) {
256                 // No sample has been queued.
257                 result = mLastBufferedPositionUs;
258                 continue;
259             }
260             if (result == null || result > lastQueuedSamplePositionUs) {
261                 result = lastQueuedSamplePositionUs;
262             }
263         }
264         if (result == null) {
265             return mLastBufferedPositionUs;
266         }
267         return (mLastBufferedPositionUs = result);
268     }
269 
270     @Override
continueBuffering(long positionUs)271     public boolean continueBuffering(long positionUs) {
272         mCurrentPlaybackPositionUs = positionUs;
273         for (int i = 0; i < mTrackCount; ++i) {
274             if (!mTrackSelected[i]) {
275                 continue;
276             }
277             SampleQueue queue = mReadSampleQueues.get(i);
278             maybeReadSample(queue, i);
279             if (queue.getLastQueuedPositionUs() == null
280                     || positionUs > queue.getLastQueuedPositionUs()) {
281                 // No more buffered data.
282                 return false;
283             }
284         }
285         return true;
286     }
287 
288     @Override
release()289     public void release() throws IOException {
290         if (mTrackCount <= 0) {
291             return;
292         }
293         if (mSampleChunkIoHelper != null) {
294             mSampleChunkIoHelper.release();
295         }
296     }
297 
298     // onChunkEvictedListener
299     @Override
onChunkEvicted(String id, long createdTimeMs)300     public void onChunkEvicted(String id, long createdTimeMs) {
301         if (mBufferListener != null) {
302             mBufferListener.onBufferStartTimeChanged(
303                     createdTimeMs + TimeUnit.MICROSECONDS.toMillis(CHUNK_DURATION_US));
304         }
305     }
306 }
307