1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 
5 package org.mozilla.gecko.media;
6 
7 import android.util.Log;
8 
9 import org.mozilla.geckoview.BuildConfig;
10 
11 import org.mozilla.thirdparty.com.google.android.exoplayer2.BaseRenderer;
12 import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
13 import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
14 import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException;
15 import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
16 import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder;
17 import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities;
18 
19 import java.nio.ByteBuffer;
20 import java.util.ArrayList;
21 import java.util.concurrent.ConcurrentLinkedQueue;
22 import java.util.Iterator;
23 
24 public abstract class GeckoHlsRendererBase extends BaseRenderer {
25     protected static final int QUEUED_INPUT_SAMPLE_DURATION_THRESHOLD = 1000000; //1sec
26     protected final FormatHolder mFormatHolder = new FormatHolder();
27     /*
28      *  DEBUG/LOGTAG will be set in the 2 subclass GeckoHlsAudioRenderer and
29      *  GeckoHlsVideoRenderer, and we still wants to log message in the base class
30      *  GeckoHlsRendererBase, so neither 'static' nor 'final' are applied to them.
31      */
32     protected boolean DEBUG;
33     protected String LOGTAG;
34     // Notify GeckoHlsPlayer about renderer's status, i.e. data has arrived.
35     protected GeckoHlsPlayer.ComponentEventDispatcher mPlayerEventDispatcher;
36 
37     protected ConcurrentLinkedQueue<GeckoHLSSample> mDemuxedInputSamples = new ConcurrentLinkedQueue<>();
38 
39     protected ByteBuffer mInputBuffer = null;
40     protected ArrayList<Format> mFormats = new ArrayList<Format>();
41     protected boolean mInitialized = false;
42     protected boolean mWaitingForData = true;
43     protected boolean mInputStreamEnded = false;
44     protected long mFirstSampleStartTime = Long.MIN_VALUE;
45 
createInputBuffer()46     protected abstract void createInputBuffer() throws ExoPlaybackException;
handleReconfiguration(DecoderInputBuffer bufferForRead)47     protected abstract void handleReconfiguration(DecoderInputBuffer bufferForRead);
handleFormatRead(DecoderInputBuffer bufferForRead)48     protected abstract void handleFormatRead(DecoderInputBuffer bufferForRead) throws ExoPlaybackException;
handleEndOfStream(DecoderInputBuffer bufferForRead)49     protected abstract void handleEndOfStream(DecoderInputBuffer bufferForRead);
handleSamplePreparation(DecoderInputBuffer bufferForRead)50     protected abstract void handleSamplePreparation(DecoderInputBuffer bufferForRead);
resetRenderer()51     protected abstract void resetRenderer();
clearInputSamplesQueue()52     protected abstract boolean clearInputSamplesQueue();
notifyPlayerInputFormatChanged(Format newFormat)53     protected abstract void notifyPlayerInputFormatChanged(Format newFormat);
54 
55     private DecoderInputBuffer mBufferForRead =
56         new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
57     private final DecoderInputBuffer mFlagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();
58 
assertTrue(final boolean condition)59     protected void assertTrue(final boolean condition) {
60         if (DEBUG && !condition) {
61             throw new AssertionError("Expected condition to be true");
62         }
63     }
64 
GeckoHlsRendererBase(final int trackType, final GeckoHlsPlayer.ComponentEventDispatcher eventDispatcher)65     public GeckoHlsRendererBase(final int trackType,
66                                 final GeckoHlsPlayer.ComponentEventDispatcher eventDispatcher) {
67         super(trackType);
68         mPlayerEventDispatcher = eventDispatcher;
69     }
70 
isQueuedEnoughData()71     private boolean isQueuedEnoughData() {
72         if (mDemuxedInputSamples.isEmpty()) {
73             return false;
74         }
75 
76         final Iterator<GeckoHLSSample> iter = mDemuxedInputSamples.iterator();
77         long firstPTS = 0;
78         if (iter.hasNext()) {
79             final GeckoHLSSample sample = iter.next();
80             firstPTS = sample.info.presentationTimeUs;
81         }
82         long lastPTS = firstPTS;
83         while (iter.hasNext()) {
84             final GeckoHLSSample sample = iter.next();
85             lastPTS = sample.info.presentationTimeUs;
86         }
87         return Math.abs(lastPTS - firstPTS) > QUEUED_INPUT_SAMPLE_DURATION_THRESHOLD;
88     }
89 
getFormat(final int index)90     public Format getFormat(final int index) {
91         assertTrue(index >= 0);
92         final Format fmt = index < mFormats.size() ? mFormats.get(index) : null;
93         if (DEBUG) {
94             Log.d(LOGTAG, "getFormat : index = " + index + ", format : " + fmt);
95         }
96         return fmt;
97     }
98 
99     public synchronized long getFirstSamplePTS() {
100         return mFirstSampleStartTime;
101     }
102 
103     public synchronized ConcurrentLinkedQueue<GeckoHLSSample> getQueuedSamples(final int number) {
104         final ConcurrentLinkedQueue<GeckoHLSSample> samples =
105             new ConcurrentLinkedQueue<GeckoHLSSample>();
106 
107         GeckoHLSSample sample = null;
108         final int queuedSize = mDemuxedInputSamples.size();
109         for (int i = 0; i < queuedSize; i++) {
110             if (i >= number) {
111                 break;
112             }
113             sample = mDemuxedInputSamples.poll();
114             samples.offer(sample);
115         }
116 
117         sample = samples.isEmpty() ? null : samples.peek();
118         if (sample == null) {
119             if (DEBUG) {
120                 Log.d(LOGTAG, "getQueuedSamples isEmpty, mWaitingForData = true !");
121             }
122             mWaitingForData = true;
123         } else if (mFirstSampleStartTime == Long.MIN_VALUE) {
124             mFirstSampleStartTime = sample.info.presentationTimeUs;
125             if (DEBUG) {
126                 Log.d(LOGTAG, "mFirstSampleStartTime = " + mFirstSampleStartTime);
127             }
128         }
129         return samples;
130     }
131 
132     protected void handleDrmInitChanged(final Format oldFormat, final Format newFormat) {
133         final Object oldDrmInit = oldFormat == null ? null : oldFormat.drmInitData;
134         final Object newDrnInit = newFormat.drmInitData;
135 
136         // TODO: Notify MFR if the content is encrypted or not.
137         if (newDrnInit != oldDrmInit) {
138             if (newDrnInit != null) {
139             } else {
140             }
141         }
142     }
143 
144     protected boolean canReconfigure(final Format oldFormat, final Format newFormat) {
145         // Referring to ExoPlayer's MediaCodecBaseRenderer, the default is set
146         // to false. Only override it in video renderer subclass.
147         return false;
148     }
149 
150     protected void prepareReconfiguration() {
151         // Referring to ExoPlayer's MediaCodec related renderers, only video
152         // renderer handles this.
153     }
154 
155     protected void updateCSDInfo(final Format format) {
156         // do nothing.
157     }
158 
159     protected void onInputFormatChanged(final Format newFormat) throws ExoPlaybackException {
160         Format oldFormat;
161         try {
162             oldFormat = mFormats.get(mFormats.size() - 1);
163         } catch (final IndexOutOfBoundsException e) {
164             oldFormat = null;
165         }
166         if (DEBUG) {
167             Log.d(LOGTAG, "[onInputFormatChanged] old : " + oldFormat +
168                   " => new : " + newFormat);
169         }
170         mFormats.add(newFormat);
171         handleDrmInitChanged(oldFormat, newFormat);
172 
173         if (mInitialized && canReconfigure(oldFormat, newFormat)) {
174             prepareReconfiguration();
175         } else {
176             resetRenderer();
177             maybeInitRenderer();
178         }
179 
180         updateCSDInfo(newFormat);
181         notifyPlayerInputFormatChanged(newFormat);
182     }
183 
184     protected void maybeInitRenderer() throws ExoPlaybackException {
185         if (mInitialized || mFormats.size() == 0) {
186             return;
187         }
188         if (DEBUG) {
189             Log.d(LOGTAG, "Initializing ... ");
190         }
191         try {
192             createInputBuffer();
193             mInitialized = true;
194         } catch (final OutOfMemoryError e) {
195             throw ExoPlaybackException.createForRenderer(new RuntimeException(e),
196                                                          getIndex(),
197                                                          mFormats.isEmpty() ? null : getFormat(mFormats.size() - 1),
198                                                          RendererCapabilities.FORMAT_HANDLED);
199         }
200     }
201 
202     /*
203      * The place we get demuxed data from HlsMediaSource(ExoPlayer).
204      * The data will then be converted to GeckoHLSSample and deliver to
205      * GeckoHlsDemuxerWrapper for further use.
206      * If the return value is ture, that means a GeckoHLSSample is queued
207      * successfully. We can try to feed more samples into queue.
208      * If the return value is false, that means we might encounter following
209      * situation 1) not initialized 2) input stream is ended 3) queue is full.
210      * 4) format changed. 5) exception happened.
211      */
212     protected synchronized boolean feedInputBuffersQueue() throws ExoPlaybackException {
213         if (!mInitialized || mInputStreamEnded || isQueuedEnoughData()) {
214             // Need to reinitialize the renderer or the input stream has ended
215             // or we just reached the maximum queue size.
216             return false;
217         }
218 
219         mBufferForRead.data = mInputBuffer;
220         if (mBufferForRead.data != null) {
221             mBufferForRead.clear();
222         }
223 
224         handleReconfiguration(mBufferForRead);
225 
226         // Read data from HlsMediaSource
227         int result = C.RESULT_NOTHING_READ;
228         try {
229             result = readSource(mFormatHolder, mBufferForRead, false);
230         } catch (final Exception e) {
231             Log.e(LOGTAG, "[feedInput] Exception when readSource :", e);
232             return false;
233         }
234 
235         if (result == C.RESULT_NOTHING_READ) {
236             return false;
237         }
238 
239         if (result == C.RESULT_FORMAT_READ) {
240             handleFormatRead(mBufferForRead);
241             return true;
242         }
243 
244         // We've read a buffer.
245         if (mBufferForRead.isEndOfStream()) {
246             if (DEBUG) {
247                 Log.d(LOGTAG, "Now we're at the End Of Stream.");
248             }
249             handleEndOfStream(mBufferForRead);
250             return false;
251         }
252 
253         mBufferForRead.flip();
254 
255         handleSamplePreparation(mBufferForRead);
256 
257         maybeNotifyDataArrived();
258         return true;
259     }
260 
261     private void maybeNotifyDataArrived() {
262         if (mWaitingForData && isQueuedEnoughData()) {
263             if (DEBUG) {
264                 Log.d(LOGTAG, "onDataArrived");
265             }
266             mPlayerEventDispatcher.onDataArrived(getTrackType());
267             mWaitingForData = false;
268         }
269     }
270 
271     private void readFormat() throws ExoPlaybackException {
272         mFlagsOnlyBuffer.clear();
273         final int result = readSource(mFormatHolder, mFlagsOnlyBuffer, true);
274         if (result == C.RESULT_FORMAT_READ) {
275             onInputFormatChanged(mFormatHolder.format);
276         }
277     }
278 
279     @Override
280     protected void onEnabled(final boolean joining) {
281         // Do nothing.
282     }
283 
284     @Override
285     protected void onDisabled() {
286         mFormats.clear();
287         resetRenderer();
288     }
289 
290     @Override
291     public boolean isReady() {
292         return mFormats.size() != 0;
293     }
294 
295     @Override
296     public boolean isEnded() {
297         return mInputStreamEnded;
298     }
299 
300     @Override
301     protected synchronized void onPositionReset(final long positionUs, final boolean joining) {
302         if (DEBUG) {
303             Log.d(LOGTAG, "onPositionReset : positionUs = " + positionUs);
304         }
305         mInputStreamEnded = false;
306         if (mInitialized) {
307             clearInputSamplesQueue();
308         }
309     }
310 
311     /*
312      * This is called by ExoPlayerImplInternal.java.
313      * ExoPlayer checks the status of renderer, i.e. isReady() / isEnded(), and
314      * calls renderer.render by passing its wall clock time.
315      */
316     @Override
317     public void render(final long positionUs, final long elapsedRealtimeUs)
318             throws ExoPlaybackException {
319         if (BuildConfig.DEBUG_BUILD) {
320             Log.d(LOGTAG, "positionUs = " + positionUs +
321                           ", mInputStreamEnded = " + mInputStreamEnded);
322         }
323         if (mInputStreamEnded) {
324             return;
325         }
326         if (mFormats.size() == 0) {
327             readFormat();
328         }
329 
330         maybeInitRenderer();
331         while (feedInputBuffersQueue()) {
332             // Do nothing
333         }
334     }
335 }
336