Source code

Revision control

Copy as Markdown

Other Tools

/* 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.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicInteger;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.annotation.ReflectionTarget;
import org.mozilla.geckoview.BuildConfig;
import org.mozilla.geckoview.GeckoSession;
import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
import org.mozilla.thirdparty.com.google.android.exoplayer2.DefaultLoadControl;
import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException;
import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer;
import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters;
import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities;
import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource;
import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener;
import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup;
import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray;
import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.HlsMediaSource;
import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection;
import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultAllocator;
import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource;
import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
@ReflectionTarget
public class GeckoHlsPlayer implements BaseHlsPlayer, ExoPlayer.EventListener {
private static final String LOGTAG = "GeckoHlsPlayer";
private static final DefaultBandwidthMeter BANDWIDTH_METER =
new DefaultBandwidthMeter.Builder(null).build();
private static final int MAX_TIMELINE_ITEM_LINES = 3;
private static final boolean DEBUG = !BuildConfig.MOZILLA_OFFICIAL;
private static final AtomicInteger sPlayerId = new AtomicInteger(0);
/*
* Because we treat GeckoHlsPlayer as a source data provider.
* It will be created and initialized with a URL by HLSResource in
* Gecko media pipleine (in cpp). Once HLSDemuxer is created later, we
* need to bridge this HLSResource to the created demuxer. And they share
* the same GeckoHlsPlayer.
* mPlayerId is a token used for Gecko media pipeline to obtain corresponding player.
*/
private final int mPlayerId;
// Accessed only in GeckoHlsPlayerThread.
private boolean mExoplayerSuspended = false;
private static final int DEFAULT_MIN_BUFFER_MS = 5 * 1000;
private static final int DEFAULT_MAX_BUFFER_MS = 10 * 1000;
private enum MediaDecoderPlayState {
PLAY_STATE_PREPARING,
PLAY_STATE_PAUSED,
PLAY_STATE_PLAYING
}
// Default value is PLAY_STATE_PREPARING and it will be set to PLAY_STATE_PLAYING
// once HTMLMediaElement calls PlayInternal().
// Accessed only in GeckoHlsPlayerThread.
private MediaDecoderPlayState mMediaDecoderPlayState = MediaDecoderPlayState.PLAY_STATE_PREPARING;
private Handler mMainHandler;
private HandlerThread mThread;
private ExoPlayer mPlayer;
private GeckoHlsRendererBase[] mRenderers;
private DefaultTrackSelector mTrackSelector;
private MediaSource mMediaSource;
private SourceEventListener mSourceEventListener;
private ComponentListener mComponentListener;
private ComponentEventDispatcher mComponentEventDispatcher;
private volatile boolean mIsTimelineStatic = false;
private long mDurationUs;
private GeckoHlsVideoRenderer mVRenderer = null;
private GeckoHlsAudioRenderer mARenderer = null;
// Able to control if we only want V/A/V+A tracks from bitstream.
private class RendererController {
private final boolean mEnableV;
private final boolean mEnableA;
RendererController(final boolean enableVideoRenderer, final boolean enableAudioRenderer) {
this.mEnableV = enableVideoRenderer;
this.mEnableA = enableAudioRenderer;
}
boolean isVideoRendererEnabled() {
return mEnableV;
}
boolean isAudioRendererEnabled() {
return mEnableA;
}
}
private RendererController mRendererController = new RendererController(true, true);
// Provide statistical information of tracks.
private class HlsMediaTracksInfo {
private int mNumVideoTracks = 0;
private int mNumAudioTracks = 0;
private boolean mVideoInfoUpdated = false;
private boolean mAudioInfoUpdated = false;
private boolean mVideoDataArrived = false;
private boolean mAudioDataArrived = false;
HlsMediaTracksInfo() {}
public void reset() {
mNumVideoTracks = 0;
mNumAudioTracks = 0;
mVideoInfoUpdated = false;
mAudioInfoUpdated = false;
mVideoDataArrived = false;
mAudioDataArrived = false;
}
public void updateNumOfVideoTracks(final int numOfTracks) {
mNumVideoTracks = numOfTracks;
}
public void updateNumOfAudioTracks(final int numOfTracks) {
mNumAudioTracks = numOfTracks;
}
public boolean hasVideo() {
return mNumVideoTracks > 0;
}
public boolean hasAudio() {
return mNumAudioTracks > 0;
}
public int getNumOfVideoTracks() {
return mNumVideoTracks;
}
public int getNumOfAudioTracks() {
return mNumAudioTracks;
}
public void onVideoInfoUpdated() {
mVideoInfoUpdated = true;
}
public void onAudioInfoUpdated() {
mAudioInfoUpdated = true;
}
public void onDataArrived(final int trackType) {
if (trackType == C.TRACK_TYPE_VIDEO) {
mVideoDataArrived = true;
} else if (trackType == C.TRACK_TYPE_AUDIO) {
mAudioDataArrived = true;
}
}
public boolean videoReady() {
return !hasVideo() || (mVideoInfoUpdated && mVideoDataArrived);
}
public boolean audioReady() {
return !hasAudio() || (mAudioInfoUpdated && mAudioDataArrived);
}
}
private HlsMediaTracksInfo mTracksInfo = new HlsMediaTracksInfo();
// Used only in GeckoHlsPlayerThread.
private boolean mIsPlayerInitDone = false;
private boolean mIsDemuxerInitDone = false;
private BaseHlsPlayer.DemuxerCallbacks mDemuxerCallbacks;
private BaseHlsPlayer.ResourceCallbacks mResourceCallbacks;
private boolean mReleasing = false; // Used only in Gecko Main thread.
private static void assertTrue(final boolean condition) {
if (DEBUG && !condition) {
throw new AssertionError("Expected condition to be true");
}
}
protected void checkInitDone() {
if (mIsDemuxerInitDone) {
return;
}
assertTrue(mDemuxerCallbacks != null);
if (DEBUG) {
Log.d(
LOGTAG,
"[checkInitDone] VReady:"
+ mTracksInfo.videoReady()
+ ",AReady:"
+ mTracksInfo.audioReady()
+ ",hasV:"
+ mTracksInfo.hasVideo()
+ ",hasA:"
+ mTracksInfo.hasAudio());
}
if (mTracksInfo.videoReady() && mTracksInfo.audioReady()) {
if (mDemuxerCallbacks != null) {
mDemuxerCallbacks.onInitialized(mTracksInfo.hasAudio(), mTracksInfo.hasVideo());
}
mIsDemuxerInitDone = true;
}
}
private final class SourceEventListener implements MediaSourceEventListener {
public void onLoadStarted(
final int windowIndex,
final MediaSource.MediaPeriodId mediaPeriodId,
final LoadEventInfo loadEventInfo,
final MediaLoadData mediaLoadData) {
assertTrue(isPlayerThread());
synchronized (GeckoHlsPlayer.this) {
if (mediaLoadData.dataType != C.DATA_TYPE_MEDIA) {
// Don't report non-media URLs.
return;
}
if (mResourceCallbacks == null || loadEventInfo.uri == null || mReleasing) {
return;
}
if (DEBUG) {
Log.d(LOGTAG, "on-load: url=" + loadEventInfo.uri);
}
mResourceCallbacks.onLoad(loadEventInfo.uri.toString());
}
}
}
public final class ComponentEventDispatcher {
// Called from GeckoHls{Audio,Video}Renderer/ExoPlayer internal playback thread
// or GeckoHlsPlayerThread.
public void onDataArrived(final int trackType) {
assertTrue(mComponentListener != null);
if (mComponentListener != null) {
runOnPlayerThread(() -> mComponentListener.onDataArrived(trackType));
}
}
// Called from GeckoHls{Audio,Video}Renderer internal playback thread.
public void onVideoInputFormatChanged(final Format format) {
assertTrue(mComponentListener != null);
if (mComponentListener != null) {
runOnPlayerThread(() -> mComponentListener.onVideoInputFormatChanged(format));
}
}
// Called from GeckoHls{Audio,Video}Renderer internal playback thread.
public void onAudioInputFormatChanged(final Format format) {
assertTrue(mComponentListener != null);
if (mComponentListener != null) {
runOnPlayerThread(() -> mComponentListener.onAudioInputFormatChanged(format));
}
}
}
public final class ComponentListener {
// General purpose implementation
// Called on GeckoHlsPlayerThread
public void onDataArrived(final int trackType) {
assertTrue(isPlayerThread());
synchronized (GeckoHlsPlayer.this) {
if (DEBUG) {
Log.d(LOGTAG, "[CB][onDataArrived] id " + mPlayerId);
}
if (!mIsPlayerInitDone) {
return;
}
mTracksInfo.onDataArrived(trackType);
if (!mReleasing) {
mResourceCallbacks.onDataArrived();
}
checkInitDone();
}
}
// Called on GeckoHlsPlayerThread
public void onVideoInputFormatChanged(final Format format) {
assertTrue(isPlayerThread());
synchronized (GeckoHlsPlayer.this) {
if (DEBUG) {
Log.d(LOGTAG, "[CB] onVideoInputFormatChanged [" + format + "]");
Log.d(
LOGTAG,
"[CB] SampleMIMEType ["
+ format.sampleMimeType
+ "], ContainerMIMEType ["
+ format.containerMimeType
+ "], id : "
+ mPlayerId);
}
if (!mIsPlayerInitDone) {
return;
}
mTracksInfo.onVideoInfoUpdated();
checkInitDone();
}
}
// Called on GeckoHlsPlayerThread
public void onAudioInputFormatChanged(final Format format) {
assertTrue(isPlayerThread());
synchronized (GeckoHlsPlayer.this) {
if (DEBUG) {
Log.d(LOGTAG, "[CB] onAudioInputFormatChanged [" + format + "], mPlayerId :" + mPlayerId);
}
if (!mIsPlayerInitDone) {
return;
}
mTracksInfo.onAudioInfoUpdated();
checkInitDone();
}
}
}
private HlsMediaSource.Factory buildDataSourceFactory(
final Context ctx, final DefaultBandwidthMeter bandwidthMeter) {
return new HlsMediaSource.Factory(
new DefaultDataSourceFactory(
ctx, bandwidthMeter, buildHttpDataSourceFactory(bandwidthMeter)));
}
private HttpDataSource.Factory buildHttpDataSourceFactory(
final DefaultBandwidthMeter bandwidthMeter) {
return new DefaultHttpDataSourceFactory(
GeckoSession.getDefaultUserAgent(),
bandwidthMeter /* listener */,
DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
true /* allowCrossProtocolRedirects */);
}
private long getDuration() {
return awaitPlayerThread(
() -> {
long duration = 0L;
// Value returned by getDuration() is in milliseconds.
if (mPlayer != null && !isLiveStream()) {
duration = Math.max(0L, mPlayer.getDuration() * 1000L);
}
if (DEBUG) {
Log.d(LOGTAG, "getDuration : " + duration + "(Us)");
}
return duration;
});
}
// To make sure that each player has a unique id, GeckoHlsPlayer should be
// created only from synchronized APIs in GeckoPlayerFactory.
public GeckoHlsPlayer() {
mPlayerId = sPlayerId.incrementAndGet();
if (DEBUG) {
Log.d(LOGTAG, " construct player with id(" + mPlayerId + ")");
}
}
// Should be only called by GeckoPlayerFactory and GeckoHLSResourceWrapper.
// The mPlayerId is used to make sure that the same GeckoHlsPlayer is used by
// corresponding HLSResource and HLSDemuxer for each media playback.
// Called on Gecko's main thread
@Override
public int getId() {
return mPlayerId;
}
// Called on Gecko's main thread
@Override
public synchronized void addDemuxerWrapperCallbackListener(
final BaseHlsPlayer.DemuxerCallbacks callback) {
if (DEBUG) {
Log.d(LOGTAG, " addDemuxerWrapperCallbackListener ...");
}
mDemuxerCallbacks = callback;
}
// Called on GeckoHlsPlayerThread from ExoPlayer
@Override
public synchronized void onLoadingChanged(final boolean isLoading) {
assertTrue(isPlayerThread());
if (DEBUG) {
Log.d(LOGTAG, "loading [" + isLoading + "]");
}
if (!isLoading) {
if (mMediaDecoderPlayState != MediaDecoderPlayState.PLAY_STATE_PLAYING) {
suspendExoplayer();
}
// To update buffered position.
mComponentEventDispatcher.onDataArrived(C.TRACK_TYPE_DEFAULT);
}
}
// Called on GeckoHlsPlayerThread from ExoPlayer
@Override
public synchronized void onPlayerStateChanged(final boolean playWhenReady, final int state) {
assertTrue(isPlayerThread());
if (DEBUG) {
Log.d(LOGTAG, "state [" + playWhenReady + ", " + getStateString(state) + "]");
}
if (state == ExoPlayer.STATE_READY
&& !mExoplayerSuspended
&& mMediaDecoderPlayState == MediaDecoderPlayState.PLAY_STATE_PLAYING) {
resumeExoplayer();
}
}
// Called on GeckoHlsPlayerThread from ExoPlayer
@Override
public void onPositionDiscontinuity(final int reason) {
assertTrue(isPlayerThread());
if (DEBUG) {
Log.d(LOGTAG, "positionDiscontinuity: reason=" + reason);
}
}
// Called on GeckoHlsPlayerThread from ExoPlayer
@Override
public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) {
assertTrue(isPlayerThread());
if (DEBUG) {
Log.d(
LOGTAG,
"playbackParameters "
+ String.format(
"[speed=%.2f, pitch=%.2f]", playbackParameters.speed, playbackParameters.pitch));
}
}
// Called on GeckoHlsPlayerThread from ExoPlayer
@Override
public synchronized void onPlayerError(final ExoPlaybackException e) {
assertTrue(isPlayerThread());
if (DEBUG) {
Log.e(LOGTAG, "playerFailed", e);
}
mIsPlayerInitDone = false;
if (mReleasing) {
return;
}
if (mResourceCallbacks != null) {
mResourceCallbacks.onError(ResourceError.PLAYER.code());
}
if (mDemuxerCallbacks != null) {
mDemuxerCallbacks.onError(DemuxerError.PLAYER.code());
}
}
// Called on GeckoHlsPlayerThread from ExoPlayer
@Override
public synchronized void onTracksChanged(
final TrackGroupArray ignored, final TrackSelectionArray trackSelections) {
assertTrue(isPlayerThread());
if (DEBUG) {
Log.d(LOGTAG, "onTracksChanged : TGA[" + ignored + "], TSA[" + trackSelections + "]");
final MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo == null) {
Log.d(LOGTAG, "Tracks []");
return;
}
Log.d(LOGTAG, "Tracks [");
// Log tracks associated to renderers.
for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.length; rendererIndex++) {
final TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);
final TrackSelection trackSelection = trackSelections.get(rendererIndex);
if (rendererTrackGroups.length > 0) {
Log.d(LOGTAG, " Renderer:" + rendererIndex + " [");
for (int groupIndex = 0; groupIndex < rendererTrackGroups.length; groupIndex++) {
final TrackGroup trackGroup = rendererTrackGroups.get(groupIndex);
final String adaptiveSupport =
getAdaptiveSupportString(
trackGroup.length,
mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false));
Log.d(
LOGTAG,
" Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " [");
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
final String status = getTrackStatusString(trackSelection, trackGroup, trackIndex);
final String formatSupport =
getFormatSupportString(
mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex));
Log.d(
LOGTAG,
" "
+ status
+ " Track:"
+ trackIndex
+ ", "
+ Format.toLogString(trackGroup.getFormat(trackIndex))
+ ", supported="
+ formatSupport);
}
Log.d(LOGTAG, " ]");
}
Log.d(LOGTAG, " ]");
}
}
// Log tracks not associated with a renderer.
final TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnassociatedTrackGroups();
if (unassociatedTrackGroups.length > 0) {
Log.d(LOGTAG, " Renderer:None [");
for (int groupIndex = 0; groupIndex < unassociatedTrackGroups.length; groupIndex++) {
Log.d(LOGTAG, " Group:" + groupIndex + " [");
final TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex);
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
final String status = getTrackStatusString(false);
final String formatSupport =
getFormatSupportString(RendererCapabilities.FORMAT_UNSUPPORTED_TYPE);
Log.d(
LOGTAG,
" "
+ status
+ " Track:"
+ trackIndex
+ ", "
+ Format.toLogString(trackGroup.getFormat(trackIndex))
+ ", supported="
+ formatSupport);
}
Log.d(LOGTAG, " ]");
}
Log.d(LOGTAG, " ]");
}
Log.d(LOGTAG, "]");
}
mTracksInfo.reset();
int numVideoTracks = 0;
int numAudioTracks = 0;
for (int j = 0; j < ignored.length; j++) {
final TrackGroup tg = ignored.get(j);
for (int i = 0; i < tg.length; i++) {
final Format fmt = tg.getFormat(i);
if (fmt.sampleMimeType != null) {
if (mRendererController.isVideoRendererEnabled()
&& fmt.sampleMimeType.startsWith(new String("video"))) {
numVideoTracks++;
} else if (mRendererController.isAudioRendererEnabled()
&& fmt.sampleMimeType.startsWith(new String("audio"))) {
numAudioTracks++;
}
}
}
}
mTracksInfo.updateNumOfVideoTracks(numVideoTracks);
mTracksInfo.updateNumOfAudioTracks(numAudioTracks);
}
// Called on GeckoHlsPlayerThread from ExoPlayer
@Override
public synchronized void onTimelineChanged(final Timeline timeline, final int reason) {
assertTrue(isPlayerThread());
// For now, we use the interface ExoPlayer.getDuration() for gecko,
// so here we create local variable 'window' & 'peroid' to obtain
// the dynamic duration.
// See.
// for further information.
final Timeline.Window window = new Timeline.Window();
mIsTimelineStatic =
!timeline.isEmpty() && !timeline.getWindow(timeline.getWindowCount() - 1, window).isDynamic;
final int periodCount = timeline.getPeriodCount();
final int windowCount = timeline.getWindowCount();
if (DEBUG) {
Log.d(LOGTAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount);
}
final Timeline.Period period = new Timeline.Period();
for (int i = 0; i < Math.min(periodCount, MAX_TIMELINE_ITEM_LINES); i++) {
timeline.getPeriod(i, period);
if (mDurationUs < period.getDurationUs()) {
mDurationUs = period.getDurationUs();
}
}
for (int i = 0; i < Math.min(windowCount, MAX_TIMELINE_ITEM_LINES); i++) {
timeline.getWindow(i, window);
if (mDurationUs < window.getDurationUs()) {
mDurationUs = window.getDurationUs();
}
}
// TODO : Need to check if the duration from play.getDuration is different
// with the one calculated from multi-timelines/windows.
if (DEBUG) {
Log.d(
LOGTAG,
"Media duration (from Timeline) = "
+ mDurationUs
+ "(us)"
+ " player.getDuration() = "
+ mPlayer.getDuration()
+ "(ms)");
}
}
private static String getStateString(final int state) {
switch (state) {
case ExoPlayer.STATE_BUFFERING:
return "B";
case ExoPlayer.STATE_ENDED:
return "E";
case ExoPlayer.STATE_IDLE:
return "I";
case ExoPlayer.STATE_READY:
return "R";
default:
return "?";
}
}
private static String getFormatSupportString(final int formatSupport) {
switch (formatSupport) {
case RendererCapabilities.FORMAT_HANDLED:
return "YES";
case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES:
return "NO_EXCEEDS_CAPABILITIES";
case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE:
return "NO_UNSUPPORTED_TYPE";
case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE:
return "NO";
default:
return "?";
}
}
private static String getAdaptiveSupportString(final int trackCount, final int adaptiveSupport) {
if (trackCount < 2) {
return "N/A";
}
switch (adaptiveSupport) {
case RendererCapabilities.ADAPTIVE_SEAMLESS:
return "YES";
case RendererCapabilities.ADAPTIVE_NOT_SEAMLESS:
return "YES_NOT_SEAMLESS";
case RendererCapabilities.ADAPTIVE_NOT_SUPPORTED:
return "NO";
default:
return "?";
}
}
private static String getTrackStatusString(
final TrackSelection selection, final TrackGroup group, final int trackIndex) {
return getTrackStatusString(
selection != null
&& selection.getTrackGroup() == group
&& selection.indexOf(trackIndex) != C.INDEX_UNSET);
}
private static String getTrackStatusString(final boolean enabled) {
return enabled ? "[X]" : "[ ]";
}
// Called on GeckoHlsPlayerThread
private void createExoPlayer(final String url) {
assertTrue(isPlayerThread());
final Context ctx = GeckoAppShell.getApplicationContext();
mComponentListener = new ComponentListener();
mComponentEventDispatcher = new ComponentEventDispatcher();
mDurationUs = 0;
// Prepare trackSelector
final TrackSelection.Factory videoTrackSelectionFactory =
new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
mTrackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
// Prepare customized renderer
mRenderers = new GeckoHlsRendererBase[2];
mVRenderer = new GeckoHlsVideoRenderer(mComponentEventDispatcher);
mARenderer = new GeckoHlsAudioRenderer(mComponentEventDispatcher);
mRenderers[0] = mVRenderer;
mRenderers[1] = mARenderer;
final DefaultLoadControl dlc =
new DefaultLoadControl.Builder()
.setAllocator(new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE))
.setBufferDurationsMs(
DEFAULT_MIN_BUFFER_MS,
DEFAULT_MAX_BUFFER_MS,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
.createDefaultLoadControl();
// Create ExoPlayer instance with specific components.
mPlayer =
new ExoPlayer.Builder(ctx, mRenderers)
.setTrackSelector(mTrackSelector)
.setLoadControl(dlc)
.build();
mPlayer.addListener(this);
final Uri uri = Uri.parse(url);
mMediaSource = buildDataSourceFactory(ctx, BANDWIDTH_METER).createMediaSource(uri);
mSourceEventListener = new SourceEventListener();
mMediaSource.addEventListener(mMainHandler, mSourceEventListener);
if (DEBUG) {
Log.d(
LOGTAG,
"Uri is " + uri + ", ContentType is " + Util.inferContentType(uri.getLastPathSegment()));
}
mPlayer.setPlayWhenReady(false);
mPlayer.prepare(mMediaSource);
mIsPlayerInitDone = true;
}
// =======================================================================
// API for GeckoHLSResourceWrapper
// =======================================================================
// Called on Gecko Main Thread
@Override
public synchronized void init(final String url, final BaseHlsPlayer.ResourceCallbacks callback) {
if (DEBUG) {
Log.d(LOGTAG, " init");
}
assertTrue(callback != null);
assertTrue(!mIsPlayerInitDone);
mThread = new HandlerThread("GeckoHlsPlayerThread");
mThread.start();
mMainHandler = new Handler(mThread.getLooper());
mMainHandler.post(
() -> {
mResourceCallbacks = callback;
createExoPlayer(url);
});
}
// Called on MDSM's TaskQueue
@Override
public boolean isLiveStream() {
return !mIsTimelineStatic;
}
// =======================================================================
// API for GeckoHLSDemuxerWrapper
// =======================================================================
// Called on HLSDemuxer's TaskQueue
@Override
public synchronized ConcurrentLinkedQueue<GeckoHLSSample> getSamples(
final TrackType trackType, final int number) {
if (trackType == TrackType.VIDEO) {
return mVRenderer != null
? mVRenderer.getQueuedSamples(number)
: new ConcurrentLinkedQueue<GeckoHLSSample>();
} else if (trackType == TrackType.AUDIO) {
return mARenderer != null
? mARenderer.getQueuedSamples(number)
: new ConcurrentLinkedQueue<GeckoHLSSample>();
} else {
return new ConcurrentLinkedQueue<GeckoHLSSample>();
}
}
// Called on MFR's TaskQueue
@Override
public long getBufferedPosition() {
return awaitPlayerThread(
() -> {
// Value returned by getBufferedPosition() is in milliseconds.
final long bufferedPos =
mPlayer == null ? 0L : Math.max(0L, mPlayer.getBufferedPosition() * 1000L);
if (DEBUG) {
Log.d(LOGTAG, "getBufferedPosition : " + bufferedPos + "(Us)");
}
return bufferedPos;
});
}
// Called on MFR's TaskQueue
@Override
public synchronized int getNumberOfTracks(final TrackType trackType) {
if (DEBUG) {
Log.d(LOGTAG, "getNumberOfTracks : type " + trackType);
}
if (trackType == TrackType.VIDEO) {
return mTracksInfo.getNumOfVideoTracks();
} else if (trackType == TrackType.AUDIO) {
return mTracksInfo.getNumOfAudioTracks();
}
return 0;
}
// Called on MFR's TaskQueue
@Override
public GeckoVideoInfo getVideoInfo(final int index) {
final Format fmt;
synchronized (this) {
if (DEBUG) {
Log.d(LOGTAG, "getVideoInfo");
}
if (mVRenderer == null) {
Log.e(LOGTAG, "no render to get video info from. Index : " + index);
return null;
}
if (!mTracksInfo.hasVideo()) {
return null;
}
fmt = mVRenderer.getFormat(index);
if (fmt == null) {
return null;
}
}
return new GeckoVideoInfo(
fmt.width,
fmt.height,
fmt.width,
fmt.height,
fmt.rotationDegrees,
fmt.stereoMode,
getDuration(),
fmt.sampleMimeType,
null,
null);
}
// Called on MFR's TaskQueue
@Override
public GeckoAudioInfo getAudioInfo(final int index) {
final Format fmt;
synchronized (this) {
if (DEBUG) {
Log.d(LOGTAG, "getAudioInfo");
}
if (mARenderer == null) {
Log.e(LOGTAG, "no render to get audio info from. Index : " + index);
return null;
}
if (!mTracksInfo.hasAudio()) {
return null;
}
fmt = mARenderer.getFormat(index);
if (fmt == null) {
return null;
}
}
* /d979469659861f7fe1d39d153b90bdff1ab479cc/library/core/src/main
* /java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java#L221-L224,
* if the input audio format is not raw, exoplayer would assure that
* the sample's pcm encoding bitdepth is 16.
* For HLS content, it should always be 16.
*/
assertTrue(!MimeTypes.AUDIO_RAW.equals(fmt.sampleMimeType));
// For HLS content, csd-0 is enough.
final byte[] csd = fmt.initializationData.isEmpty() ? null : fmt.initializationData.get(0);
return new GeckoAudioInfo(
fmt.sampleRate, fmt.channelCount, 16, 0, getDuration(), fmt.sampleMimeType, csd);
}
// Called on HLSDemuxer's TaskQueue
@Override
public boolean seek(final long positionUs) {
synchronized (this) {
if (mPlayer == null) {
Log.d(LOGTAG, "Seek operation won't be performed as no player exists!");
return false;
}
}
return awaitPlayerThread(
() -> {
// Need to temporarily resume Exoplayer to download the chunks for getting the demuxed
// keyframe sample when HTMLMediaElement is paused. Suspend Exoplayer when collecting
// enough
// samples in onLoadingChanged.
if (mExoplayerSuspended) {
resumeExoplayer();
}
// positionUs : microseconds.
// NOTE : 1) It's not possible to seek media by tracktype via ExoPlayer Interface.
// 2) positionUs is samples PTS from MFR, we need to re-adjust it
// for ExoPlayer by subtracting sample start time.
// 3) Time unit for ExoPlayer.seek() is milliseconds.
try {
// TODO : Gather Timeline Period / Window information to develop
// complete timeline, and seekTime should be inside the duration.
Long startTime = Long.MAX_VALUE;
for (final GeckoHlsRendererBase r : mRenderers) {
if (r == mVRenderer
&& mRendererController.isVideoRendererEnabled()
&& mTracksInfo.hasVideo()
|| r == mARenderer
&& mRendererController.isAudioRendererEnabled()
&& mTracksInfo.hasAudio()) {
// Find the min value of the start time
startTime = Math.min(startTime, r.getFirstSamplePTS());
}
}
if (DEBUG) {
Log.d(
LOGTAG,
"seeking : "
+ positionUs / 1000
+ " (ms); startTime : "
+ startTime / 1000
+ " (ms)");
}
assertTrue(startTime != Long.MAX_VALUE && startTime != Long.MIN_VALUE);
mPlayer.seekTo(positionUs / 1000 - startTime / 1000);
} catch (final Exception e) {
if (mReleasing) {
return false;
}
if (mDemuxerCallbacks != null) {
mDemuxerCallbacks.onError(DemuxerError.UNKNOWN.code());
}
return false;
}
return true;
});
}
// Called on HLSDemuxer's TaskQueue
@Override
public synchronized long getNextKeyFrameTime() {
return mVRenderer != null ? mVRenderer.getNextKeyFrameTime() : Long.MAX_VALUE;
}
// Called on Gecko's main thread.
@Override
public synchronized void suspend() {
runOnPlayerThread(
() -> {
if (mExoplayerSuspended) {
return;
}
if (mMediaDecoderPlayState != MediaDecoderPlayState.PLAY_STATE_PLAYING) {
if (DEBUG) {
Log.d(LOGTAG, "suspend player id : " + mPlayerId);
}
suspendExoplayer();
}
});
}
// Called on Gecko's main thread.
@Override
public synchronized void resume() {
runOnPlayerThread(
() -> {
if (!mExoplayerSuspended) {
return;
}
if (mMediaDecoderPlayState == MediaDecoderPlayState.PLAY_STATE_PLAYING) {
if (DEBUG) {
Log.d(LOGTAG, "resume player id : " + mPlayerId);
}
resumeExoplayer();
}
});
}
// Called on Gecko's main thread.
@Override
public synchronized void play() {
runOnPlayerThread(
() -> {
if (mMediaDecoderPlayState == MediaDecoderPlayState.PLAY_STATE_PLAYING) {
return;
}
if (DEBUG) {
Log.d(LOGTAG, "MediaDecoder played.");
}
mMediaDecoderPlayState = MediaDecoderPlayState.PLAY_STATE_PLAYING;
resumeExoplayer();
});
}
// Called on Gecko's main thread.
@Override
public synchronized void pause() {
runOnPlayerThread(
() -> {
if (mMediaDecoderPlayState != MediaDecoderPlayState.PLAY_STATE_PLAYING) {
return;
}
if (DEBUG) {
Log.d(LOGTAG, "MediaDecoder paused.");
}
mMediaDecoderPlayState = MediaDecoderPlayState.PLAY_STATE_PAUSED;
suspendExoplayer();
});
}
private void suspendExoplayer() {
assertTrue(isPlayerThread());
if (mPlayer == null) {
return;
}
mExoplayerSuspended = true;
if (DEBUG) {
Log.d(LOGTAG, "suspend Exoplayer");
}
mPlayer.setPlayWhenReady(false);
}
private void resumeExoplayer() {
assertTrue(isPlayerThread());
if (mPlayer == null) {
return;
}
mExoplayerSuspended = false;
if (DEBUG) {
Log.d(LOGTAG, "resume Exoplayer");
}
mPlayer.setPlayWhenReady(true);
}
// Called on Gecko's main thread, when HLSDemuxer or HLSResource destructs.
@Override
public void release() {
if (DEBUG) {
Log.d(LOGTAG, "releasing ... id : " + mPlayerId);
}
synchronized (this) {
if (mReleasing) {
return;
} else {
mReleasing = true;
}
}
runOnPlayerThread(
() -> {
if (mPlayer != null) {
mPlayer.removeListener(this);
mPlayer.stop();
mPlayer.release();
mVRenderer = null;
mARenderer = null;
mPlayer = null;
}
if (mThread != null) {
mThread.quit();
mThread = null;
}
mDemuxerCallbacks = null;
mResourceCallbacks = null;
mIsPlayerInitDone = false;
mIsDemuxerInitDone = false;
});
}
private void runOnPlayerThread(final Runnable task) {
assertTrue(mMainHandler != null);
if (isPlayerThread()) {
task.run();
} else {
mMainHandler.post(task);
}
}
private boolean isPlayerThread() {
return Thread.currentThread() == mMainHandler.getLooper().getThread();
}
private <T> T awaitPlayerThread(final Callable<T> task) {
assertTrue(!isPlayerThread());
try {
final FutureTask<T> wait = new FutureTask<T>(task);
mMainHandler.post(wait);
return wait.get();
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
}