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