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.content.Context;
8 import android.net.Uri;
9 import android.os.Handler;
10 import android.os.HandlerThread;
11 import android.util.Log;
12 
13 import com.google.android.exoplayer2.C;
14 import com.google.android.exoplayer2.DefaultLoadControl;
15 import com.google.android.exoplayer2.ExoPlaybackException;
16 import com.google.android.exoplayer2.ExoPlayer;
17 import com.google.android.exoplayer2.ExoPlayerFactory;
18 import com.google.android.exoplayer2.Format;
19 import com.google.android.exoplayer2.PlaybackParameters;
20 import com.google.android.exoplayer2.RendererCapabilities;
21 import com.google.android.exoplayer2.Timeline;
22 import com.google.android.exoplayer2.source.MediaSource;
23 import com.google.android.exoplayer2.source.TrackGroup;
24 import com.google.android.exoplayer2.source.TrackGroupArray;
25 import com.google.android.exoplayer2.source.hls.HlsMediaSource;
26 import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
27 import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
28 import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
29 import com.google.android.exoplayer2.trackselection.TrackSelection;
30 import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
31 import com.google.android.exoplayer2.upstream.DataSource;
32 import com.google.android.exoplayer2.upstream.DefaultAllocator;
33 import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
34 import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
35 import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
36 import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
37 import com.google.android.exoplayer2.upstream.HttpDataSource;
38 import com.google.android.exoplayer2.util.MimeTypes;
39 import com.google.android.exoplayer2.util.Util;
40 
41 import org.mozilla.gecko.GeckoAppShell;
42 import org.mozilla.gecko.annotation.ReflectionTarget;
43 import org.mozilla.geckoview.BuildConfig;
44 
45 import java.util.concurrent.ConcurrentLinkedQueue;
46 import java.util.concurrent.atomic.AtomicInteger;
47 
48 @ReflectionTarget
49 public class GeckoHlsPlayer implements BaseHlsPlayer, ExoPlayer.EventListener {
50     private static final String LOGTAG = "GeckoHlsPlayer";
51     private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
52     private static final int MAX_TIMELINE_ITEM_LINES = 3;
53     private static final boolean DEBUG = !BuildConfig.MOZILLA_OFFICIAL;
54 
55     private static final AtomicInteger sPlayerId = new AtomicInteger(0);
56     /*
57      *  Because we treat GeckoHlsPlayer as a source data provider.
58      *  It will be created and initialized with a URL by HLSResource in
59      *  Gecko media pipleine (in cpp). Once HLSDemuxer is created later, we
60      *  need to bridge this HLSResource to the created demuxer. And they share
61      *  the same GeckoHlsPlayer.
62      *  mPlayerId is a token used for Gecko media pipeline to obtain corresponding player.
63      */
64     private final int mPlayerId;
65     private boolean mExoplayerSuspended = false;
66 
67     private enum MediaDecoderPlayState {
68         PLAY_STATE_PREPARING,
69         PLAY_STATE_PAUSED,
70         PLAY_STATE_PLAYING
71     }
72     // Default value is PLAY_STATE_PREPARING and it will be set to PLAY_STATE_PLAYING
73     // once HTMLMediaElement calls PlayInternal().
74     private MediaDecoderPlayState mMediaDecoderPlayState = MediaDecoderPlayState.PLAY_STATE_PREPARING;
75     private DataSource.Factory mMediaDataSourceFactory;
76 
77     private Handler mMainHandler;
78     private HandlerThread mThread;
79     private ExoPlayer mPlayer;
80     private GeckoHlsRendererBase[] mRenderers;
81     private DefaultTrackSelector mTrackSelector;
82     private MediaSource mMediaSource;
83     private ComponentListener mComponentListener;
84     private ComponentEventDispatcher mComponentEventDispatcher;
85 
86     private volatile boolean mIsTimelineStatic = false;
87     private long mDurationUs;
88 
89     private GeckoHlsVideoRenderer mVRenderer = null;
90     private GeckoHlsAudioRenderer mARenderer = null;
91 
92     // Able to control if we only want V/A/V+A tracks from bitstream.
93     private class RendererController {
94         private final boolean mEnableV;
95         private final boolean mEnableA;
RendererController(boolean enableVideoRenderer, boolean enableAudioRenderer)96         RendererController(boolean enableVideoRenderer, boolean enableAudioRenderer) {
97             this.mEnableV = enableVideoRenderer;
98             this.mEnableA = enableAudioRenderer;
99         }
isVideoRendererEnabled()100         boolean isVideoRendererEnabled() { return mEnableV; }
isAudioRendererEnabled()101         boolean isAudioRendererEnabled() { return mEnableA; }
102     }
103     private RendererController mRendererController = new RendererController(true, true);
104 
105     // Provide statistical information of tracks.
106     private class HlsMediaTracksInfo {
107         private int mNumVideoTracks = 0;
108         private int mNumAudioTracks = 0;
109         private boolean mVideoInfoUpdated = false;
110         private boolean mAudioInfoUpdated = false;
111         private boolean mVideoDataArrived = false;
112         private boolean mAudioDataArrived = false;
HlsMediaTracksInfo()113         HlsMediaTracksInfo() {}
reset()114         public void reset() {
115             mNumVideoTracks = 0;
116             mNumAudioTracks = 0;
117             mVideoInfoUpdated = false;
118             mAudioInfoUpdated = false;
119             mVideoDataArrived = false;
120             mAudioDataArrived = false;
121         }
updateNumOfVideoTracks(int numOfTracks)122         public void updateNumOfVideoTracks(int numOfTracks) { mNumVideoTracks = numOfTracks; }
updateNumOfAudioTracks(int numOfTracks)123         public void updateNumOfAudioTracks(int numOfTracks) { mNumAudioTracks = numOfTracks; }
hasVideo()124         public boolean hasVideo() { return mNumVideoTracks > 0; }
hasAudio()125         public boolean hasAudio() { return mNumAudioTracks > 0; }
getNumOfVideoTracks()126         public int getNumOfVideoTracks() { return mNumVideoTracks; }
getNumOfAudioTracks()127         public int getNumOfAudioTracks() { return mNumAudioTracks; }
onVideoInfoUpdated()128         public void onVideoInfoUpdated() { mVideoInfoUpdated = true; }
onAudioInfoUpdated()129         public void onAudioInfoUpdated() { mAudioInfoUpdated = true; }
onDataArrived(int trackType)130         public void onDataArrived(int trackType) {
131             if (trackType == C.TRACK_TYPE_VIDEO) {
132                 mVideoDataArrived = true;
133             } else if (trackType == C.TRACK_TYPE_AUDIO) {
134                 mAudioDataArrived = true;
135             }
136         }
videoReady()137         public boolean videoReady() {
138             return !hasVideo() || (mVideoInfoUpdated && mVideoDataArrived);
139         }
audioReady()140         public boolean audioReady() {
141             return !hasAudio() || (mAudioInfoUpdated && mAudioDataArrived);
142         }
143     }
144     private HlsMediaTracksInfo mTracksInfo = new HlsMediaTracksInfo();
145 
146     private boolean mIsPlayerInitDone = false;
147     private boolean mIsDemuxerInitDone = false;
148 
149     private BaseHlsPlayer.DemuxerCallbacks mDemuxerCallbacks;
150     private BaseHlsPlayer.ResourceCallbacks mResourceCallbacks;
151 
assertTrue(boolean condition)152     private static void assertTrue(boolean condition) {
153       if (DEBUG && !condition) {
154         throw new AssertionError("Expected condition to be true");
155       }
156     }
157 
checkInitDone()158     protected void checkInitDone() {
159         if (mIsDemuxerInitDone) {
160             return;
161         }
162         assertTrue(mDemuxerCallbacks != null);
163 
164         if (DEBUG) {
165             Log.d(LOGTAG, "[checkInitDone] VReady:" + mTracksInfo.videoReady() +
166                     ",AReady:" + mTracksInfo.audioReady() +
167                     ",hasV:" + mTracksInfo.hasVideo() +
168                     ",hasA:" + mTracksInfo.hasAudio());
169         }
170         if (mTracksInfo.videoReady() && mTracksInfo.audioReady()) {
171             if (mDemuxerCallbacks != null) {
172                 mDemuxerCallbacks.onInitialized(mTracksInfo.hasAudio(), mTracksInfo.hasVideo());
173             }
174             mIsDemuxerInitDone = true;
175         }
176     }
177 
178     public final class ComponentEventDispatcher {
179         // Called on GeckoHlsPlayerThread from GeckoHls{Audio,Video}Renderer/ExoPlayer
onDataArrived(final int trackType)180         public void onDataArrived(final int trackType) {
181             assertTrue(mMainHandler != null);
182             assertTrue(mComponentListener != null);
183 
184             if (mMainHandler != null && mComponentListener != null) {
185                 mMainHandler.post(new Runnable() {
186                     @Override
187                     public void run() {
188                         mComponentListener.onDataArrived(trackType);
189                     }
190                 });
191             }
192         }
193 
194         // Called on GeckoHlsPlayerThread from GeckoHls{Audio,Video}Renderer
onVideoInputFormatChanged(final Format format)195         public void onVideoInputFormatChanged(final Format format) {
196             assertTrue(mMainHandler != null);
197             assertTrue(mComponentListener != null);
198 
199             if (mMainHandler != null && mComponentListener != null) {
200                 mMainHandler.post(new Runnable() {
201                     @Override
202                     public void run() {
203                         mComponentListener.onVideoInputFormatChanged(format);
204                     }
205                 });
206             }
207         }
208 
209         // Called on GeckoHlsPlayerThread from GeckoHls{Audio,Video}Renderer
onAudioInputFormatChanged(final Format format)210         public void onAudioInputFormatChanged(final Format format) {
211             assertTrue(mMainHandler != null);
212             assertTrue(mComponentListener != null);
213 
214             if (mMainHandler != null && mComponentListener != null) {
215                 mMainHandler.post(new Runnable() {
216                     @Override
217                     public void run() {
218                         mComponentListener.onAudioInputFormatChanged(format);
219                     }
220                 });
221             }
222         }
223     }
224 
225     public final class ComponentListener {
226 
227         // General purpose implementation
228         // Called on GeckoHlsPlayerThread
onDataArrived(int trackType)229         public void onDataArrived(int trackType) {
230             synchronized (GeckoHlsPlayer.this) {
231                 if (DEBUG) { Log.d(LOGTAG, "[CB][onDataArrived] id " + mPlayerId); }
232                 if (!mIsPlayerInitDone) {
233                     return;
234                 }
235                 mTracksInfo.onDataArrived(trackType);
236                 mResourceCallbacks.onDataArrived();
237                 checkInitDone();
238             }
239         }
240 
241         // Called on GeckoHlsPlayerThread
onVideoInputFormatChanged(Format format)242         public void onVideoInputFormatChanged(Format format) {
243             synchronized (GeckoHlsPlayer.this) {
244                 if (DEBUG) {
245                     Log.d(LOGTAG, "[CB] onVideoInputFormatChanged [" + format + "]");
246                     Log.d(LOGTAG, "[CB] SampleMIMEType [" +
247                             format.sampleMimeType + "], ContainerMIMEType [" +
248                             format.containerMimeType + "], id : " + mPlayerId);
249                 }
250                 if (!mIsPlayerInitDone) {
251                     return;
252                 }
253                 mTracksInfo.onVideoInfoUpdated();
254                 checkInitDone();
255             }
256         }
257 
258         // Called on GeckoHlsPlayerThread
onAudioInputFormatChanged(Format format)259         public void onAudioInputFormatChanged(Format format) {
260             synchronized (GeckoHlsPlayer.this) {
261                 if (DEBUG) {
262                     Log.d(LOGTAG, "[CB] onAudioInputFormatChanged [" + format + "], mPlayerId :" + mPlayerId);
263                 }
264                 if (!mIsPlayerInitDone) {
265                     return;
266                 }
267                 mTracksInfo.onAudioInfoUpdated();
268                 checkInitDone();
269             }
270         }
271     }
272 
buildDataSourceFactory(Context ctx, DefaultBandwidthMeter bandwidthMeter)273     private DataSource.Factory buildDataSourceFactory(Context ctx, DefaultBandwidthMeter bandwidthMeter) {
274         return new DefaultDataSourceFactory(ctx, bandwidthMeter,
275                 buildHttpDataSourceFactory(bandwidthMeter));
276     }
277 
buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter)278     private HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
279         return new DefaultHttpDataSourceFactory(
280             BuildConfig.USER_AGENT_GECKOVIEW_MOBILE,
281             bandwidthMeter /* listener */,
282             DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
283             DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
284             true /* allowCrossProtocolRedirects */
285         );
286     }
287 
getDuration()288     private synchronized long getDuration() {
289         long duration = 0L;
290         // Value returned by getDuration() is in milliseconds.
291         if (mPlayer != null && !isLiveStream()) {
292             duration = Math.max(0L, mPlayer.getDuration() * 1000L);
293         }
294         if (DEBUG) { Log.d(LOGTAG, "getDuration : " + duration  + "(Us)"); }
295         return duration;
296     }
297 
298     // To make sure that each player has a unique id, GeckoHlsPlayer should be
299     // created only from synchronized APIs in GeckoPlayerFactory.
GeckoHlsPlayer()300     public GeckoHlsPlayer() {
301         mPlayerId = sPlayerId.incrementAndGet();
302         if (DEBUG) { Log.d(LOGTAG, " construct player with id(" + mPlayerId + ")"); }
303     }
304 
305     // Should be only called by GeckoPlayerFactory and GeckoHLSResourceWrapper.
306     // The mPlayerId is used to make sure that the same GeckoHlsPlayer is used by
307     // corresponding HLSResource and HLSDemuxer for each media playback.
308     // Called on Gecko's main thread
309     @Override
getId()310     public int getId() {
311         return mPlayerId;
312     }
313 
314     // Called on Gecko's main thread
315     @Override
addDemuxerWrapperCallbackListener(BaseHlsPlayer.DemuxerCallbacks callback)316     public synchronized void addDemuxerWrapperCallbackListener(BaseHlsPlayer.DemuxerCallbacks callback) {
317         if (DEBUG) { Log.d(LOGTAG, " addDemuxerWrapperCallbackListener ..."); }
318         mDemuxerCallbacks = callback;
319     }
320 
321     // Called on GeckoHlsPlayerThread from ExoPlayer
322     @Override
onLoadingChanged(boolean isLoading)323     public synchronized void onLoadingChanged(boolean isLoading) {
324         if (DEBUG) { Log.d(LOGTAG, "loading [" + isLoading + "]"); }
325         if (!isLoading) {
326             if (mMediaDecoderPlayState != MediaDecoderPlayState.PLAY_STATE_PLAYING) {
327                 suspendExoplayer();
328             }
329             // To update buffered position.
330             mComponentEventDispatcher.onDataArrived(C.TRACK_TYPE_DEFAULT);
331         }
332     }
333 
334     // Called on GeckoHlsPlayerThread from ExoPlayer
335     @Override
onPlayerStateChanged(boolean playWhenReady, int state)336     public synchronized void onPlayerStateChanged(boolean playWhenReady, int state) {
337         if (DEBUG) { Log.d(LOGTAG, "state [" + playWhenReady + ", " + getStateString(state) + "]"); }
338         if (state == ExoPlayer.STATE_READY &&
339             !mExoplayerSuspended &&
340             mMediaDecoderPlayState == MediaDecoderPlayState.PLAY_STATE_PLAYING) {
341             resumeExoplayer();
342         }
343     }
344 
345     // Called on GeckoHlsPlayerThread from ExoPlayer
346     @Override
onPositionDiscontinuity()347     public void onPositionDiscontinuity() {
348         if (DEBUG) { Log.d(LOGTAG, "positionDiscontinuity"); }
349     }
350 
351     // Called on GeckoHlsPlayerThread from ExoPlayer
352     @Override
onPlaybackParametersChanged(PlaybackParameters playbackParameters)353     public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
354         if (DEBUG) {
355             Log.d(LOGTAG, "playbackParameters " +
356                   String.format("[speed=%.2f, pitch=%.2f]", playbackParameters.speed, playbackParameters.pitch));
357         }
358     }
359 
360     // Called on GeckoHlsPlayerThread from ExoPlayer
361     @Override
onPlayerError(ExoPlaybackException e)362     public synchronized void onPlayerError(ExoPlaybackException e) {
363         if (DEBUG) { Log.e(LOGTAG, "playerFailed" , e); }
364         mIsPlayerInitDone = false;
365         if (mResourceCallbacks != null) {
366             mResourceCallbacks.onError(ResourceError.PLAYER.code());
367         }
368         if (mDemuxerCallbacks != null) {
369             mDemuxerCallbacks.onError(DemuxerError.PLAYER.code());
370         }
371     }
372 
373     // Called on GeckoHlsPlayerThread from ExoPlayer
374     @Override
onTracksChanged(TrackGroupArray ignored, TrackSelectionArray trackSelections)375     public synchronized void onTracksChanged(TrackGroupArray ignored, TrackSelectionArray trackSelections) {
376         if (DEBUG) {
377             Log.d(LOGTAG, "onTracksChanged : TGA[" + ignored +
378                           "], TSA[" + trackSelections + "]");
379 
380             MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo();
381             if (mappedTrackInfo == null) {
382                 Log.d(LOGTAG, "Tracks []");
383                 return;
384             }
385             Log.d(LOGTAG, "Tracks [");
386             // Log tracks associated to renderers.
387             for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.length; rendererIndex++) {
388                 TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);
389                 TrackSelection trackSelection = trackSelections.get(rendererIndex);
390                 if (rendererTrackGroups.length > 0) {
391                     Log.d(LOGTAG, "  Renderer:" + rendererIndex + " [");
392                     for (int groupIndex = 0; groupIndex < rendererTrackGroups.length; groupIndex++) {
393                         TrackGroup trackGroup = rendererTrackGroups.get(groupIndex);
394                         String adaptiveSupport = getAdaptiveSupportString(trackGroup.length,
395                                 mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false));
396                         Log.d(LOGTAG, "    Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " [");
397                         for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
398                             String status = getTrackStatusString(trackSelection, trackGroup, trackIndex);
399                             String formatSupport = getFormatSupportString(
400                                     mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex));
401                             Log.d(LOGTAG, "      " + status + " Track:" + trackIndex + ", " +
402                                     Format.toLogString(trackGroup.getFormat(trackIndex)) +
403                                     ", supported=" + formatSupport);
404                         }
405                         Log.d(LOGTAG, "    ]");
406                     }
407                     Log.d(LOGTAG, "  ]");
408                 }
409             }
410             // Log tracks not associated with a renderer.
411             TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnassociatedTrackGroups();
412             if (unassociatedTrackGroups.length > 0) {
413                 Log.d(LOGTAG, "  Renderer:None [");
414                 for (int groupIndex = 0; groupIndex < unassociatedTrackGroups.length; groupIndex++) {
415                     Log.d(LOGTAG, "    Group:" + groupIndex + " [");
416                     TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex);
417                     for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
418                         String status = getTrackStatusString(false);
419                         String formatSupport = getFormatSupportString(
420                                 RendererCapabilities.FORMAT_UNSUPPORTED_TYPE);
421                         Log.d(LOGTAG, "      " + status + " Track:" + trackIndex +
422                                 ", " + Format.toLogString(trackGroup.getFormat(trackIndex)) +
423                                 ", supported=" + formatSupport);
424                     }
425                     Log.d(LOGTAG, "    ]");
426                 }
427                 Log.d(LOGTAG, "  ]");
428             }
429             Log.d(LOGTAG, "]");
430         }
431         mTracksInfo.reset();
432         int numVideoTracks = 0;
433         int numAudioTracks = 0;
434         for (int j = 0; j < ignored.length; j++) {
435             TrackGroup tg = ignored.get(j);
436             for (int i = 0; i < tg.length; i++) {
437                 Format fmt = tg.getFormat(i);
438                 if (fmt.sampleMimeType != null) {
439                     if (mRendererController.isVideoRendererEnabled() &&
440                         fmt.sampleMimeType.startsWith(new String("video"))) {
441                         numVideoTracks++;
442                     } else if (mRendererController.isAudioRendererEnabled() &&
443                                fmt.sampleMimeType.startsWith(new String("audio"))) {
444                         numAudioTracks++;
445                     }
446                 }
447             }
448         }
449         mTracksInfo.updateNumOfVideoTracks(numVideoTracks);
450         mTracksInfo.updateNumOfAudioTracks(numAudioTracks);
451     }
452 
453     // Called on GeckoHlsPlayerThread from ExoPlayer
454     @Override
onTimelineChanged(Timeline timeline, Object manifest)455     public synchronized void onTimelineChanged(Timeline timeline, Object manifest) {
456         // For now, we use the interface ExoPlayer.getDuration() for gecko,
457         // so here we create local variable 'window' & 'peroid' to obtain
458         // the dynamic duration.
459         // See. http://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Timeline.html
460         // for further information.
461         Timeline.Window window = new Timeline.Window();
462         mIsTimelineStatic = !timeline.isEmpty()
463                 && !timeline.getWindow(timeline.getWindowCount() - 1, window).isDynamic;
464 
465         int periodCount = timeline.getPeriodCount();
466         int windowCount = timeline.getWindowCount();
467         if (DEBUG) { Log.d(LOGTAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount); }
468         Timeline.Period period = new Timeline.Period();
469         for (int i = 0; i < Math.min(periodCount, MAX_TIMELINE_ITEM_LINES); i++) {
470           timeline.getPeriod(i, period);
471           if (mDurationUs < period.getDurationUs()) {
472               mDurationUs = period.getDurationUs();
473           }
474         }
475         for (int i = 0; i < Math.min(windowCount, MAX_TIMELINE_ITEM_LINES); i++) {
476           timeline.getWindow(i, window);
477           if (mDurationUs < window.getDurationUs()) {
478               mDurationUs = window.getDurationUs();
479           }
480         }
481         // TODO : Need to check if the duration from play.getDuration is different
482         // with the one calculated from multi-timelines/windows.
483         if (DEBUG) {
484             Log.d(LOGTAG, "Media duration (from Timeline) = " + mDurationUs +
485                           "(us)" + " player.getDuration() = " + mPlayer.getDuration() +
486                           "(ms)");
487         }
488     }
489 
getStateString(int state)490     private static String getStateString(int state) {
491         switch (state) {
492             case ExoPlayer.STATE_BUFFERING:
493                 return "B";
494             case ExoPlayer.STATE_ENDED:
495                 return "E";
496             case ExoPlayer.STATE_IDLE:
497                 return "I";
498             case ExoPlayer.STATE_READY:
499                 return "R";
500             default:
501                 return "?";
502         }
503     }
504 
getFormatSupportString(int formatSupport)505     private static String getFormatSupportString(int formatSupport) {
506         switch (formatSupport) {
507           case RendererCapabilities.FORMAT_HANDLED:
508             return "YES";
509           case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES:
510             return "NO_EXCEEDS_CAPABILITIES";
511           case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE:
512             return "NO_UNSUPPORTED_TYPE";
513           case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE:
514             return "NO";
515           default:
516             return "?";
517         }
518       }
519 
getAdaptiveSupportString(int trackCount, int adaptiveSupport)520     private static String getAdaptiveSupportString(int trackCount, int adaptiveSupport) {
521         if (trackCount < 2) {
522           return "N/A";
523         }
524         switch (adaptiveSupport) {
525           case RendererCapabilities.ADAPTIVE_SEAMLESS:
526             return "YES";
527           case RendererCapabilities.ADAPTIVE_NOT_SEAMLESS:
528             return "YES_NOT_SEAMLESS";
529           case RendererCapabilities.ADAPTIVE_NOT_SUPPORTED:
530             return "NO";
531           default:
532             return "?";
533         }
534       }
535 
getTrackStatusString(TrackSelection selection, TrackGroup group, int trackIndex)536       private static String getTrackStatusString(TrackSelection selection, TrackGroup group,
537                                                  int trackIndex) {
538         return getTrackStatusString(selection != null && selection.getTrackGroup() == group
539                 && selection.indexOf(trackIndex) != C.INDEX_UNSET);
540       }
541 
getTrackStatusString(boolean enabled)542       private static String getTrackStatusString(boolean enabled) {
543         return enabled ? "[X]" : "[ ]";
544       }
545 
546     // Called on GeckoHlsPlayerThread
createExoPlayer(final String url)547     private synchronized void createExoPlayer(final String url) {
548         Context ctx = GeckoAppShell.getApplicationContext();
549         mComponentListener = new ComponentListener();
550         mComponentEventDispatcher = new ComponentEventDispatcher();
551         mDurationUs = 0;
552 
553         // Prepare trackSelector
554         TrackSelection.Factory videoTrackSelectionFactory =
555                 new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
556         mTrackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
557 
558         // Prepare customized renderer
559         mRenderers = new GeckoHlsRendererBase[2];
560         mVRenderer = new GeckoHlsVideoRenderer(mComponentEventDispatcher);
561         mARenderer = new GeckoHlsAudioRenderer(mComponentEventDispatcher);
562         mRenderers[0] = mVRenderer;
563         mRenderers[1] = mARenderer;
564 
565         // Use default values for constructing DefaultLoadControl except maxBufferMs.
566         // See Bug 1424168.
567         int maxBufferMs = Math.max(DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
568                                    DefaultLoadControl.DEFAULT_MAX_BUFFER_MS / 2);
569         DefaultLoadControl dlc =
570             new DefaultLoadControl(
571                 new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
572                 DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
573                 maxBufferMs, /*this value can eliminate the memory usage immensely by experiment*/
574                 DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
575                 DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
576         // Create ExoPlayer instance with specific components.
577         mPlayer = ExoPlayerFactory.newInstance(mRenderers, mTrackSelector, dlc);
578         mPlayer.addListener(this);
579 
580         Uri uri = Uri.parse(url);
581         mMediaDataSourceFactory = buildDataSourceFactory(ctx, BANDWIDTH_METER);
582         mMediaSource = new HlsMediaSource(uri, mMediaDataSourceFactory, mMainHandler, null);
583         if (DEBUG) {
584             Log.d(LOGTAG, "Uri is " + uri +
585                           ", ContentType is " + Util.inferContentType(uri.getLastPathSegment()));
586         }
587         mPlayer.setPlayWhenReady(false);
588         mPlayer.prepare(mMediaSource);
589         mIsPlayerInitDone = true;
590     }
591     // =======================================================================
592     // API for GeckoHLSResourceWrapper
593     // =======================================================================
594     // Called on Gecko Main Thread
595     @Override
init(final String url, BaseHlsPlayer.ResourceCallbacks callback)596     public synchronized void init(final String url, BaseHlsPlayer.ResourceCallbacks callback) {
597         if (DEBUG) { Log.d(LOGTAG, " init"); }
598         assertTrue(callback != null);
599         assertTrue(!mIsPlayerInitDone);
600 
601         mResourceCallbacks = callback;
602         mThread = new HandlerThread("GeckoHlsPlayerThread");
603         mThread.start();
604         mMainHandler = new Handler(mThread.getLooper());
605 
606         mMainHandler.post(new Runnable() {
607             @Override
608             public void run() {
609                 createExoPlayer(url);
610             }
611         });
612     }
613 
614     // Called on MDSM's TaskQueue
615     @Override
isLiveStream()616     public boolean isLiveStream() {
617         return !mIsTimelineStatic;
618     }
619     // =======================================================================
620     // API for GeckoHLSDemuxerWrapper
621     // =======================================================================
622     // Called on HLSDemuxer's TaskQueue
623     @Override
getSamples(TrackType trackType, int number)624     public synchronized ConcurrentLinkedQueue<GeckoHLSSample> getSamples(TrackType trackType,
625                                                             int number) {
626         if (trackType == TrackType.VIDEO) {
627             return mVRenderer != null ? mVRenderer.getQueuedSamples(number) :
628                                         new ConcurrentLinkedQueue<GeckoHLSSample>();
629         } else if (trackType == TrackType.AUDIO) {
630             return mARenderer != null ? mARenderer.getQueuedSamples(number) :
631                                         new ConcurrentLinkedQueue<GeckoHLSSample>();
632         } else {
633             return new ConcurrentLinkedQueue<GeckoHLSSample>();
634         }
635     }
636 
637     // Called on MFR's TaskQueue
638     @Override
getBufferedPosition()639     public synchronized long getBufferedPosition() {
640         // Value returned by getBufferedPosition() is in milliseconds.
641         long bufferedPos = mPlayer == null ? 0L : Math.max(0L, mPlayer.getBufferedPosition() * 1000L);
642         if (DEBUG) { Log.d(LOGTAG, "getBufferedPosition : " + bufferedPos + "(Us)"); }
643         return bufferedPos;
644     }
645 
646     // Called on MFR's TaskQueue
647     @Override
getNumberOfTracks(TrackType trackType)648     public synchronized int getNumberOfTracks(TrackType trackType) {
649         if (DEBUG) { Log.d(LOGTAG, "getNumberOfTracks : type " + trackType); }
650         if (trackType == TrackType.VIDEO) {
651             return mTracksInfo.getNumOfVideoTracks();
652         } else if (trackType == TrackType.AUDIO) {
653             return mTracksInfo.getNumOfAudioTracks();
654         }
655         return 0;
656     }
657 
658     // Called on MFR's TaskQueue
659     @Override
getVideoInfo(int index)660     public synchronized GeckoVideoInfo getVideoInfo(int index) {
661         if (DEBUG) { Log.d(LOGTAG, "getVideoInfo"); }
662         assertTrue(mVRenderer != null);
663         if (!mTracksInfo.hasVideo()) {
664             return null;
665         }
666         Format fmt = mVRenderer.getFormat(index);
667         if (fmt == null) {
668             return null;
669         }
670         GeckoVideoInfo vInfo = new GeckoVideoInfo(fmt.width, fmt.height,
671                                                   fmt.width, fmt.height,
672                                                   fmt.rotationDegrees, fmt.stereoMode,
673                                                   getDuration(), fmt.sampleMimeType,
674                                                   null, null);
675         return vInfo;
676     }
677 
678     // Called on MFR's TaskQueue
679     @Override
getAudioInfo(int index)680     public synchronized GeckoAudioInfo getAudioInfo(int index) {
681         if (DEBUG) { Log.d(LOGTAG, "getAudioInfo"); }
682         assertTrue(mARenderer != null);
683         if (!mTracksInfo.hasAudio()) {
684             return null;
685         }
686         Format fmt = mARenderer.getFormat(index);
687         if (fmt == null) {
688             return null;
689         }
690         /* According to https://github.com/google/ExoPlayer/blob
691          * /d979469659861f7fe1d39d153b90bdff1ab479cc/library/core/src/main
692          * /java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java#L221-L224,
693          * if the input audio format is not raw, exoplayer would assure that
694          * the sample's pcm encoding bitdepth is 16.
695          * For HLS content, it should always be 16.
696          */
697         assertTrue(!MimeTypes.AUDIO_RAW.equals(fmt.sampleMimeType));
698         // For HLS content, csd-0 is enough.
699         byte[] csd = fmt.initializationData.isEmpty() ? null : fmt.initializationData.get(0);
700         GeckoAudioInfo aInfo = new GeckoAudioInfo(fmt.sampleRate, fmt.channelCount,
701                                                   16, 0, getDuration(),
702                                                   fmt.sampleMimeType, csd);
703         return aInfo;
704     }
705 
706     // Called on HLSDemuxer's TaskQueue
707     @Override
seek(long positionUs)708     public synchronized boolean seek(long positionUs) {
709         if (mPlayer == null) {
710             Log.d(LOGTAG, "Seek operation won't be performed as no player exists!");
711             return false;
712         }
713 
714         // Need to temporarily resume Exoplayer to download the chunks for getting the demuxed
715         // keyframe sample when HTMLMediaElement is paused. Suspend Exoplayer when collecting enough
716         // samples in onLoadingChanged.
717         if (mExoplayerSuspended) {
718             resumeExoplayer();
719         }
720         // positionUs : microseconds.
721         // NOTE : 1) It's not possible to seek media by tracktype via ExoPlayer Interface.
722         //        2) positionUs is samples PTS from MFR, we need to re-adjust it
723         //           for ExoPlayer by subtracting sample start time.
724         //        3) Time unit for ExoPlayer.seek() is milliseconds.
725         try {
726             // TODO : Gather Timeline Period / Window information to develop
727             //        complete timeline, and seekTime should be inside the duration.
728             Long startTime = Long.MAX_VALUE;
729             for (GeckoHlsRendererBase r : mRenderers) {
730                 if (r == mVRenderer && mRendererController.isVideoRendererEnabled() && mTracksInfo.hasVideo() ||
731                     r == mARenderer && mRendererController.isAudioRendererEnabled() && mTracksInfo.hasAudio()) {
732                 // Find the min value of the start time
733                     startTime = Math.min(startTime, r.getFirstSamplePTS());
734                 }
735             }
736             if (DEBUG) {
737                 Log.d(LOGTAG, "seeking  : " + positionUs / 1000 +
738                               " (ms); startTime : " + startTime / 1000 + " (ms)");
739             }
740             assertTrue(startTime != Long.MAX_VALUE && startTime != Long.MIN_VALUE);
741             mPlayer.seekTo(positionUs / 1000 - startTime / 1000);
742         } catch (Exception e) {
743             if (mDemuxerCallbacks != null) {
744                 mDemuxerCallbacks.onError(DemuxerError.UNKNOWN.code());
745             }
746             return false;
747         }
748         return true;
749     }
750 
751     // Called on HLSDemuxer's TaskQueue
752     @Override
getNextKeyFrameTime()753     public synchronized long getNextKeyFrameTime() {
754         long nextKeyFrameTime = mVRenderer != null
755             ? mVRenderer.getNextKeyFrameTime()
756             : Long.MAX_VALUE;
757         return nextKeyFrameTime;
758     }
759 
760     // Called on Gecko's main thread.
761     @Override
suspend()762     public synchronized void suspend() {
763         if (mExoplayerSuspended) {
764             return;
765         }
766         if (mMediaDecoderPlayState != MediaDecoderPlayState.PLAY_STATE_PLAYING) {
767             if (DEBUG) {
768                 Log.d(LOGTAG, "suspend player id : " + mPlayerId);
769             }
770             suspendExoplayer();
771         }
772     }
773 
774     // Called on Gecko's main thread.
775     @Override
resume()776     public synchronized void resume() {
777         if (!mExoplayerSuspended) {
778           return;
779         }
780         if (mMediaDecoderPlayState == MediaDecoderPlayState.PLAY_STATE_PLAYING) {
781             if (DEBUG) {
782                 Log.d(LOGTAG, "resume player id : " + mPlayerId);
783             }
784             resumeExoplayer();
785         }
786     }
787 
788     // Called on Gecko's main thread.
789     @Override
play()790     public synchronized void play() {
791         if (mMediaDecoderPlayState == MediaDecoderPlayState.PLAY_STATE_PLAYING) {
792             return;
793         }
794         if (DEBUG) { Log.d(LOGTAG, "MediaDecoder played."); }
795         mMediaDecoderPlayState = MediaDecoderPlayState.PLAY_STATE_PLAYING;
796         resumeExoplayer();
797     }
798 
799     // Called on Gecko's main thread.
800     @Override
pause()801     public synchronized void pause() {
802         if (mMediaDecoderPlayState != MediaDecoderPlayState.PLAY_STATE_PLAYING) {
803             return;
804         }
805         if (DEBUG) { Log.d(LOGTAG, "MediaDecoder paused."); }
806         mMediaDecoderPlayState = MediaDecoderPlayState.PLAY_STATE_PAUSED;
807         suspendExoplayer();
808     }
809 
suspendExoplayer()810     private synchronized void suspendExoplayer() {
811         if (mPlayer != null) {
812             mExoplayerSuspended = true;
813             if (DEBUG) { Log.d(LOGTAG, "suspend Exoplayer"); }
814             mPlayer.setPlayWhenReady(false);
815         }
816     }
817 
resumeExoplayer()818     private synchronized void resumeExoplayer() {
819         if (mPlayer != null) {
820             mExoplayerSuspended = false;
821             if (DEBUG) { Log.d(LOGTAG, "resume Exoplayer"); }
822             mPlayer.setPlayWhenReady(true);
823         }
824     }
825     // Called on Gecko's main thread, when HLSDemuxer or HLSResource destructs.
826     @Override
release()827     public synchronized void release() {
828         if (DEBUG) { Log.d(LOGTAG, "releasing  ... id : " + mPlayerId); }
829         if (mPlayer != null) {
830             mPlayer.removeListener(this);
831             mPlayer.stop();
832             mPlayer.release();
833             mVRenderer = null;
834             mARenderer = null;
835             mPlayer = null;
836         }
837         if (mThread != null) {
838             mThread.quit();
839             mThread = null;
840         }
841         mDemuxerCallbacks = null;
842         mResourceCallbacks = null;
843         mIsPlayerInitDone = false;
844         mIsDemuxerInitDone = false;
845     }
846 }
847