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