/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.media; import android.util.Log; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Iterator; import java.util.concurrent.ConcurrentLinkedQueue; import org.mozilla.geckoview.BuildConfig; import org.mozilla.thirdparty.com.google.android.exoplayer2.BaseRenderer; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; public abstract class GeckoHlsRendererBase extends BaseRenderer { protected static final int QUEUED_INPUT_SAMPLE_DURATION_THRESHOLD = 1000000; // 1sec protected final FormatHolder mFormatHolder = new FormatHolder(); /* * DEBUG/LOGTAG will be set in the 2 subclass GeckoHlsAudioRenderer and * GeckoHlsVideoRenderer, and we still wants to log message in the base class * GeckoHlsRendererBase, so neither 'static' nor 'final' are applied to them. */ protected boolean DEBUG; protected String LOGTAG; // Notify GeckoHlsPlayer about renderer's status, i.e. data has arrived. protected GeckoHlsPlayer.ComponentEventDispatcher mPlayerEventDispatcher; protected ConcurrentLinkedQueue mDemuxedInputSamples = new ConcurrentLinkedQueue<>(); protected ByteBuffer mInputBuffer = null; protected ArrayList mFormats = new ArrayList(); protected boolean mInitialized = false; protected boolean mWaitingForData = true; protected boolean mInputStreamEnded = false; protected long mFirstSampleStartTime = Long.MIN_VALUE; protected abstract void createInputBuffer() throws ExoPlaybackException; protected abstract void handleReconfiguration(DecoderInputBuffer bufferForRead); protected abstract void handleFormatRead(DecoderInputBuffer bufferForRead) throws ExoPlaybackException; protected abstract void handleEndOfStream(DecoderInputBuffer bufferForRead); protected abstract void handleSamplePreparation(DecoderInputBuffer bufferForRead); protected abstract void resetRenderer(); protected abstract boolean clearInputSamplesQueue(); protected abstract void notifyPlayerInputFormatChanged(Format newFormat); private DecoderInputBuffer mBufferForRead = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); private final DecoderInputBuffer mFlagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); protected void assertTrue(final boolean condition) { if (DEBUG && !condition) { throw new AssertionError("Expected condition to be true"); } } public GeckoHlsRendererBase( final int trackType, final GeckoHlsPlayer.ComponentEventDispatcher eventDispatcher) { super(trackType); mPlayerEventDispatcher = eventDispatcher; } private boolean isQueuedEnoughData() { if (mDemuxedInputSamples.isEmpty()) { return false; } final Iterator iter = mDemuxedInputSamples.iterator(); long firstPTS = 0; if (iter.hasNext()) { final GeckoHLSSample sample = iter.next(); firstPTS = sample.info.presentationTimeUs; } long lastPTS = firstPTS; while (iter.hasNext()) { final GeckoHLSSample sample = iter.next(); lastPTS = sample.info.presentationTimeUs; } return Math.abs(lastPTS - firstPTS) > QUEUED_INPUT_SAMPLE_DURATION_THRESHOLD; } public Format getFormat(final int index) { assertTrue(index >= 0); final Format fmt = index < mFormats.size() ? mFormats.get(index) : null; if (DEBUG) { Log.d(LOGTAG, "getFormat : index = " + index + ", format : " + fmt); } return fmt; } public synchronized long getFirstSamplePTS() { return mFirstSampleStartTime; } public synchronized ConcurrentLinkedQueue getQueuedSamples(final int number) { final ConcurrentLinkedQueue samples = new ConcurrentLinkedQueue(); GeckoHLSSample sample = null; final int queuedSize = mDemuxedInputSamples.size(); for (int i = 0; i < queuedSize; i++) { if (i >= number) { break; } sample = mDemuxedInputSamples.poll(); samples.offer(sample); } sample = samples.isEmpty() ? null : samples.peek(); if (sample == null) { if (DEBUG) { Log.d(LOGTAG, "getQueuedSamples isEmpty, mWaitingForData = true !"); } mWaitingForData = true; } else if (mFirstSampleStartTime == Long.MIN_VALUE) { mFirstSampleStartTime = sample.info.presentationTimeUs; if (DEBUG) { Log.d(LOGTAG, "mFirstSampleStartTime = " + mFirstSampleStartTime); } } return samples; } protected void handleDrmInitChanged(final Format oldFormat, final Format newFormat) { final Object oldDrmInit = oldFormat == null ? null : oldFormat.drmInitData; final Object newDrnInit = newFormat.drmInitData; // TODO: Notify MFR if the content is encrypted or not. if (newDrnInit != oldDrmInit) { if (newDrnInit != null) { } else { } } } protected boolean canReconfigure(final Format oldFormat, final Format newFormat) { // Referring to ExoPlayer's MediaCodecBaseRenderer, the default is set // to false. Only override it in video renderer subclass. return false; } protected void prepareReconfiguration() { // Referring to ExoPlayer's MediaCodec related renderers, only video // renderer handles this. } protected void updateCSDInfo(final Format format) { // do nothing. } protected void onInputFormatChanged(final Format newFormat) throws ExoPlaybackException { Format oldFormat; try { oldFormat = mFormats.get(mFormats.size() - 1); } catch (final IndexOutOfBoundsException e) { oldFormat = null; } if (DEBUG) { Log.d(LOGTAG, "[onInputFormatChanged] old : " + oldFormat + " => new : " + newFormat); } mFormats.add(newFormat); handleDrmInitChanged(oldFormat, newFormat); if (mInitialized && canReconfigure(oldFormat, newFormat)) { prepareReconfiguration(); } else { resetRenderer(); maybeInitRenderer(); } updateCSDInfo(newFormat); notifyPlayerInputFormatChanged(newFormat); } protected void maybeInitRenderer() throws ExoPlaybackException { if (mInitialized || mFormats.size() == 0) { return; } if (DEBUG) { Log.d(LOGTAG, "Initializing ... "); } try { createInputBuffer(); mInitialized = true; } catch (final OutOfMemoryError e) { throw ExoPlaybackException.createForRenderer( new RuntimeException(e), getIndex(), mFormats.isEmpty() ? null : getFormat(mFormats.size() - 1), RendererCapabilities.FORMAT_HANDLED); } } /* * The place we get demuxed data from HlsMediaSource(ExoPlayer). * The data will then be converted to GeckoHLSSample and deliver to * GeckoHlsDemuxerWrapper for further use. * If the return value is ture, that means a GeckoHLSSample is queued * successfully. We can try to feed more samples into queue. * If the return value is false, that means we might encounter following * situation 1) not initialized 2) input stream is ended 3) queue is full. * 4) format changed. 5) exception happened. */ protected synchronized boolean feedInputBuffersQueue() throws ExoPlaybackException { if (!mInitialized || mInputStreamEnded || isQueuedEnoughData()) { // Need to reinitialize the renderer or the input stream has ended // or we just reached the maximum queue size. return false; } mBufferForRead.data = mInputBuffer; if (mBufferForRead.data != null) { mBufferForRead.clear(); } handleReconfiguration(mBufferForRead); // Read data from HlsMediaSource int result = C.RESULT_NOTHING_READ; try { result = readSource(mFormatHolder, mBufferForRead, false); } catch (final Exception e) { Log.e(LOGTAG, "[feedInput] Exception when readSource :", e); return false; } if (result == C.RESULT_NOTHING_READ) { return false; } if (result == C.RESULT_FORMAT_READ) { handleFormatRead(mBufferForRead); return true; } // We've read a buffer. if (mBufferForRead.isEndOfStream()) { if (DEBUG) { Log.d(LOGTAG, "Now we're at the End Of Stream."); } handleEndOfStream(mBufferForRead); return false; } mBufferForRead.flip(); handleSamplePreparation(mBufferForRead); maybeNotifyDataArrived(); return true; } private void maybeNotifyDataArrived() { if (mWaitingForData && isQueuedEnoughData()) { if (DEBUG) { Log.d(LOGTAG, "onDataArrived"); } mPlayerEventDispatcher.onDataArrived(getTrackType()); mWaitingForData = false; } } private void readFormat() throws ExoPlaybackException { mFlagsOnlyBuffer.clear(); final int result = readSource(mFormatHolder, mFlagsOnlyBuffer, true); if (result == C.RESULT_FORMAT_READ) { onInputFormatChanged(mFormatHolder.format); } } @Override protected void onEnabled(final boolean joining) { // Do nothing. } @Override protected void onDisabled() { mFormats.clear(); resetRenderer(); } @Override public boolean isReady() { return mFormats.size() != 0; } @Override public boolean isEnded() { return mInputStreamEnded; } @Override protected synchronized void onPositionReset(final long positionUs, final boolean joining) { if (DEBUG) { Log.d(LOGTAG, "onPositionReset : positionUs = " + positionUs); } mInputStreamEnded = false; if (mInitialized) { clearInputSamplesQueue(); } } /* * This is called by ExoPlayerImplInternal.java. * ExoPlayer checks the status of renderer, i.e. isReady() / isEnded(), and * calls renderer.render by passing its wall clock time. */ @Override public void render(final long positionUs, final long elapsedRealtimeUs) throws ExoPlaybackException { if (BuildConfig.DEBUG_BUILD) { Log.d(LOGTAG, "positionUs = " + positionUs + ", mInputStreamEnded = " + mInputStreamEnded); } if (mInputStreamEnded) { return; } if (mFormats.size() == 0) { readFormat(); } maybeInitRenderer(); while (feedInputBuffersQueue()) { // Do nothing } } }