1 /*
2  * Copyright (C) 2015 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;
18 
19 import android.net.Uri;
20 import android.os.ConditionVariable;
21 import android.os.Handler;
22 import android.os.HandlerThread;
23 import android.os.Looper;
24 import android.os.Message;
25 import android.os.SystemClock;
26 
27 import com.google.android.exoplayer.C;
28 import com.google.android.exoplayer.MediaFormat;
29 import com.google.android.exoplayer.MediaFormatHolder;
30 import com.google.android.exoplayer.SampleHolder;
31 import com.google.android.exoplayer.SampleSource;
32 import com.google.android.exoplayer.extractor.ExtractorSampleSource;
33 import com.google.android.exoplayer.extractor.ExtractorSampleSource.EventListener;
34 import com.google.android.exoplayer.upstream.Allocator;
35 import com.google.android.exoplayer.upstream.DataSource;
36 import com.google.android.exoplayer.upstream.DefaultAllocator;
37 import com.android.tv.tuner.exoplayer.buffer.BufferManager;
38 import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer;
39 import com.android.tv.tuner.exoplayer.buffer.SimpleSampleBuffer;
40 import com.android.tv.tuner.tvinput.PlaybackBufferListener;
41 
42 import java.io.IOException;
43 import java.util.ArrayList;
44 import java.util.HashMap;
45 import java.util.List;
46 import java.util.Locale;
47 import java.util.concurrent.atomic.AtomicBoolean;
48 import java.util.concurrent.atomic.AtomicLong;
49 
50 /**
51  * A class that extracts samples from a live broadcast stream while storing the sample on the disk.
52  * For demux, this class relies on {@link com.google.android.exoplayer.extractor.ts.TsExtractor}.
53  */
54 public class ExoPlayerSampleExtractor implements SampleExtractor {
55     private static final String TAG = "ExoPlayerSampleExtracto";
56 
57     // Buffer segment size for memory allocator. Copied from demo implementation of ExoPlayer.
58     private static final int BUFFER_SEGMENT_SIZE_IN_BYTES = 64 * 1024;
59     // Buffer segment count for sample source. Copied from demo implementation of ExoPlayer.
60     private static final int BUFFER_SEGMENT_COUNT = 256;
61 
62     private final HandlerThread mSourceReaderThread;
63     private final long mId;
64 
65     private final Handler.Callback mSourceReaderWorker;
66 
67     private BufferManager.SampleBuffer mSampleBuffer;
68     private Handler mSourceReaderHandler;
69     private volatile boolean mPrepared;
70     private AtomicBoolean mOnCompletionCalled = new AtomicBoolean();
71     private IOException mExceptionOnPrepare;
72     private List<MediaFormat> mTrackFormats;
73     private HashMap<Integer, Long> mLastExtractedPositionUsMap = new HashMap<>();
74     private OnCompletionListener mOnCompletionListener;
75     private Handler mOnCompletionListenerHandler;
76     private IOException mError;
77 
ExoPlayerSampleExtractor(Uri uri, DataSource source, BufferManager bufferManager, PlaybackBufferListener bufferListener, boolean isRecording)78     public ExoPlayerSampleExtractor(Uri uri, DataSource source, BufferManager bufferManager,
79             PlaybackBufferListener bufferListener, boolean isRecording) {
80         // It'll be used as a timeshift file chunk name's prefix.
81         mId = System.currentTimeMillis();
82         Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE_IN_BYTES);
83 
84         EventListener eventListener = new EventListener() {
85 
86             @Override
87             public void onLoadError(int sourceId, IOException e) {
88                 mError = e;
89             }
90         };
91 
92         mSourceReaderThread = new HandlerThread("SourceReaderThread");
93         mSourceReaderWorker = new SourceReaderWorker(new ExtractorSampleSource(uri, source,
94                 allocator, BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE_IN_BYTES,
95                 // Do not create a handler if we not on a looper. e.g. test.
96                 Looper.myLooper() != null ? new Handler() : null,
97                 eventListener, 0));
98         if (isRecording) {
99             mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, false,
100                     RecordingSampleBuffer.BUFFER_REASON_RECORDING);
101         } else {
102             if (bufferManager == null || bufferManager.isDisabled()) {
103                 mSampleBuffer = new SimpleSampleBuffer(bufferListener);
104             } else {
105                 mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, true,
106                         RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK);
107             }
108         }
109     }
110 
111     @Override
setOnCompletionListener(OnCompletionListener listener, Handler handler)112     public void setOnCompletionListener(OnCompletionListener listener, Handler handler) {
113         mOnCompletionListener = listener;
114         mOnCompletionListenerHandler = handler;
115     }
116 
117     private class SourceReaderWorker implements Handler.Callback {
118         public static final int MSG_PREPARE = 1;
119         public static final int MSG_FETCH_SAMPLES = 2;
120         public static final int MSG_RELEASE = 3;
121         private static final int RETRY_INTERVAL_MS = 50;
122 
123         private final SampleSource mSampleSource;
124         private SampleSource.SampleSourceReader mSampleSourceReader;
125         private boolean[] mTrackMetEos;
126         private boolean mMetEos = false;
127         private long mCurrentPosition;
128 
SourceReaderWorker(SampleSource sampleSource)129         public SourceReaderWorker(SampleSource sampleSource) {
130             mSampleSource = sampleSource;
131         }
132 
133         @Override
handleMessage(Message message)134         public boolean handleMessage(Message message) {
135             switch (message.what) {
136                 case MSG_PREPARE:
137                     mPrepared = prepare();
138                     if (!mPrepared && mExceptionOnPrepare == null) {
139                             mSourceReaderHandler
140                                     .sendEmptyMessageDelayed(MSG_PREPARE, RETRY_INTERVAL_MS);
141                     } else{
142                         mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES);
143                     }
144                     return true;
145                 case MSG_FETCH_SAMPLES:
146                     boolean didSomething = false;
147                     SampleHolder sample = new SampleHolder(
148                             SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
149                     ConditionVariable conditionVariable = new ConditionVariable();
150                     int trackCount = mSampleSourceReader.getTrackCount();
151                     for (int i = 0; i < trackCount; ++i) {
152                         if (!mTrackMetEos[i] && SampleSource.NOTHING_READ
153                                 != fetchSample(i, sample, conditionVariable)) {
154                             if (mMetEos) {
155                                 // If mMetEos was on during fetchSample() due to an error,
156                                 // fetching from other tracks is not necessary.
157                                 break;
158                             }
159                             didSomething = true;
160                         }
161                     }
162                     if (!mMetEos) {
163                         if (didSomething) {
164                             mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES);
165                         } else {
166                             mSourceReaderHandler.sendEmptyMessageDelayed(MSG_FETCH_SAMPLES,
167                                     RETRY_INTERVAL_MS);
168                         }
169                     } else {
170                         notifyCompletionIfNeeded(false);
171                     }
172                     return true;
173                 case MSG_RELEASE:
174                     if (mSampleSourceReader != null) {
175                         if (mPrepared) {
176                             // ExtractorSampleSource expects all the tracks should be disabled
177                             // before releasing.
178                             int count = mSampleSourceReader.getTrackCount();
179                             for (int i = 0; i < count; ++i) {
180                                 mSampleSourceReader.disable(i);
181                             }
182                         }
183                         mSampleSourceReader.release();
184                         mSampleSourceReader = null;
185                     }
186                     cleanUp();
187                     mSourceReaderHandler.removeCallbacksAndMessages(null);
188                     return true;
189             }
190             return false;
191         }
192 
prepare()193         private boolean prepare() {
194             if (mSampleSourceReader == null) {
195                 mSampleSourceReader = mSampleSource.register();
196             }
197             if(!mSampleSourceReader.prepare(0)) {
198                 return false;
199             }
200             if (mTrackFormats == null) {
201                 int trackCount = mSampleSourceReader.getTrackCount();
202                 mTrackMetEos = new boolean[trackCount];
203                 List<MediaFormat> trackFormats = new ArrayList<>();
204                 for (int i = 0; i < trackCount; i++) {
205                     trackFormats.add(mSampleSourceReader.getFormat(i));
206                     mSampleSourceReader.enable(i, 0);
207 
208                 }
209                 mTrackFormats = trackFormats;
210                 List<String> ids = new ArrayList<>();
211                 for (int i = 0; i < mTrackFormats.size(); i++) {
212                     ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i));
213                 }
214                 try {
215                     mSampleBuffer.init(ids, mTrackFormats);
216                 } catch (IOException e) {
217                     // In this case, we will not schedule any further operation.
218                     // mExceptionOnPrepare will be notified to ExoPlayer, and ExoPlayer will
219                     // call release() eventually.
220                     mExceptionOnPrepare = e;
221                     return false;
222                 }
223             }
224             return true;
225         }
226 
fetchSample(int track, SampleHolder sample, ConditionVariable conditionVariable)227         private int fetchSample(int track, SampleHolder sample,
228                 ConditionVariable conditionVariable) {
229             mSampleSourceReader.continueBuffering(track, mCurrentPosition);
230 
231             MediaFormatHolder formatHolder = new MediaFormatHolder();
232             sample.clearData();
233             int ret = mSampleSourceReader.readData(track, mCurrentPosition, formatHolder, sample);
234             if (ret == SampleSource.SAMPLE_READ) {
235                 if (mCurrentPosition < sample.timeUs) {
236                     mCurrentPosition = sample.timeUs;
237                 }
238                 try {
239                     Long lastExtractedPositionUs = mLastExtractedPositionUsMap.get(track);
240                     if (lastExtractedPositionUs == null) {
241                         mLastExtractedPositionUsMap.put(track, sample.timeUs);
242                     } else {
243                         mLastExtractedPositionUsMap.put(track,
244                                 Math.max(lastExtractedPositionUs, sample.timeUs));
245                     }
246                     queueSample(track, sample, conditionVariable);
247                 } catch (IOException e) {
248                     mLastExtractedPositionUsMap.clear();
249                     mMetEos = true;
250                     mSampleBuffer.setEos();
251                 }
252             } else if (ret == SampleSource.END_OF_STREAM) {
253                 mTrackMetEos[track] = true;
254                 for (int i = 0; i < mTrackMetEos.length; ++i) {
255                     if (!mTrackMetEos[i]) {
256                         break;
257                     }
258                     if (i == mTrackMetEos.length -1) {
259                         mMetEos = true;
260                         mSampleBuffer.setEos();
261                     }
262                 }
263             }
264             // TODO: Handle SampleSource.FORMAT_READ for dynamic resolution change. b/28169263
265             return ret;
266         }
267     }
268 
queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable)269     private void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
270             throws IOException {
271         long writeStartTimeNs = SystemClock.elapsedRealtimeNanos();
272         mSampleBuffer.writeSample(index, sample, conditionVariable);
273 
274         // Checks whether the storage has enough bandwidth for recording samples.
275         if (mSampleBuffer.isWriteSpeedSlow(sample.size,
276                 SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) {
277             mSampleBuffer.handleWriteSpeedSlow();
278         }
279     }
280 
281     @Override
maybeThrowError()282     public void maybeThrowError() throws IOException {
283         if (mError != null) {
284             IOException e = mError;
285             mError = null;
286             throw e;
287         }
288     }
289 
290     @Override
prepare()291     public boolean prepare() throws IOException {
292         if (!mSourceReaderThread.isAlive()) {
293             mSourceReaderThread.start();
294             mSourceReaderHandler = new Handler(mSourceReaderThread.getLooper(),
295                     mSourceReaderWorker);
296             mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_PREPARE);
297         }
298         if (mExceptionOnPrepare != null) {
299             throw mExceptionOnPrepare;
300         }
301         return mPrepared;
302     }
303 
304     @Override
getTrackFormats()305     public List<MediaFormat> getTrackFormats() {
306         return mTrackFormats;
307     }
308 
309     @Override
getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder)310     public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) {
311         outMediaFormatHolder.format = mTrackFormats.get(track);
312         outMediaFormatHolder.drmInitData = null;
313     }
314 
315     @Override
selectTrack(int index)316     public void selectTrack(int index) {
317         mSampleBuffer.selectTrack(index);
318     }
319 
320     @Override
deselectTrack(int index)321     public void deselectTrack(int index) {
322         mSampleBuffer.deselectTrack(index);
323     }
324 
325     @Override
getBufferedPositionUs()326     public long getBufferedPositionUs() {
327         return mSampleBuffer.getBufferedPositionUs();
328     }
329 
330     @Override
continueBuffering(long positionUs)331     public boolean continueBuffering(long positionUs)  {
332         return mSampleBuffer.continueBuffering(positionUs);
333     }
334 
335     @Override
seekTo(long positionUs)336     public void seekTo(long positionUs) {
337         mSampleBuffer.seekTo(positionUs);
338     }
339 
340     @Override
readSample(int track, SampleHolder sampleHolder)341     public int readSample(int track, SampleHolder sampleHolder) {
342         return mSampleBuffer.readSample(track, sampleHolder);
343     }
344 
345     @Override
release()346     public void release() {
347         if (mSourceReaderThread.isAlive()) {
348             mSourceReaderHandler.removeCallbacksAndMessages(null);
349             mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_RELEASE);
350             mSourceReaderThread.quitSafely();
351             // Return early in this case so that session worker can start working on the next
352             // request as early as it can. The clean up will be done in the reader thread while
353             // handling MSG_RELEASE.
354         } else {
355             cleanUp();
356         }
357     }
358 
cleanUp()359     private void cleanUp() {
360         boolean result = true;
361         try {
362             if (mSampleBuffer != null) {
363                 mSampleBuffer.release();
364                 mSampleBuffer = null;
365             }
366         } catch (IOException e) {
367             result = false;
368         }
369         notifyCompletionIfNeeded(result);
370         setOnCompletionListener(null, null);
371     }
372 
notifyCompletionIfNeeded(final boolean result)373     private void notifyCompletionIfNeeded(final boolean result) {
374         if (!mOnCompletionCalled.getAndSet(true)) {
375             final OnCompletionListener listener = mOnCompletionListener;
376             final long lastExtractedPositionUs = getLastExtractedPositionUs();
377             if (mOnCompletionListenerHandler != null && mOnCompletionListener != null) {
378                 mOnCompletionListenerHandler.post(new Runnable() {
379                     @Override
380                     public void run() {
381                         listener.onCompletion(result, lastExtractedPositionUs);
382                     }
383                 });
384             }
385         }
386     }
387 
getLastExtractedPositionUs()388     private long getLastExtractedPositionUs() {
389         long lastExtractedPositionUs = Long.MAX_VALUE;
390         for (long value : mLastExtractedPositionUsMap.values()) {
391             lastExtractedPositionUs = Math.min(lastExtractedPositionUs, value);
392         }
393         if (lastExtractedPositionUs == Long.MAX_VALUE) {
394             lastExtractedPositionUs = C.UNKNOWN_TIME_US;
395         }
396         return lastExtractedPositionUs;
397     }
398 }
399