1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tv.tuner.exoplayer2; 18 19 import android.content.Context; 20 import android.media.PlaybackParams; 21 import android.net.Uri; 22 import android.support.annotation.IntDef; 23 import android.support.annotation.Nullable; 24 import android.view.Surface; 25 26 import com.android.tv.common.SoftPreconditions; 27 import com.android.tv.tuner.data.Cea708Data; 28 import com.android.tv.tuner.data.Cea708Data.CaptionEvent; 29 import com.android.tv.tuner.data.Cea708Parser; 30 import com.android.tv.tuner.data.TunerChannel; 31 import com.android.tv.tuner.source.TsDataSource; 32 import com.android.tv.tuner.source.TsDataSourceManager; 33 import com.android.tv.tuner.ts.EventDetector; 34 import com.android.tv.tuner.tvinput.debug.TunerDebug; 35 import com.google.android.exoplayer2.C; 36 import com.google.android.exoplayer2.ExoPlaybackException; 37 import com.google.android.exoplayer2.ExoPlayer; 38 import com.google.android.exoplayer2.ExoPlayerFactory; 39 import com.google.android.exoplayer2.Format; 40 import com.google.android.exoplayer2.Player; 41 import com.google.android.exoplayer2.SimpleExoPlayer; 42 import com.google.android.exoplayer2.Timeline; 43 import com.google.android.exoplayer2.audio.AudioListener; 44 import com.google.android.exoplayer2.source.MediaSource; 45 import com.google.android.exoplayer2.source.ProgressiveMediaSource; 46 import com.google.android.exoplayer2.source.TrackGroup; 47 import com.google.android.exoplayer2.source.TrackGroupArray; 48 import com.google.android.exoplayer2.text.Cue; 49 import com.google.android.exoplayer2.text.TextOutput; 50 import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; 51 import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; 52 import com.google.android.exoplayer2.trackselection.TrackSelection; 53 import com.google.android.exoplayer2.trackselection.TrackSelectionArray; 54 import com.google.android.exoplayer2.video.VideoListener; 55 import com.google.android.exoplayer2.video.VideoRendererEventListener; 56 57 import java.lang.annotation.Retention; 58 import java.lang.annotation.RetentionPolicy; 59 import java.util.List; 60 61 /** MPEG-2 TS stream player implementation using ExoPlayer2. */ 62 public class MpegTsPlayerV2 63 implements Player.EventListener, 64 VideoListener, 65 AudioListener, 66 TextOutput, 67 VideoRendererEventListener { 68 69 /** Interface definition for a callback to be notified of changes in player state. */ 70 public interface Callback { 71 /** 72 * Called when player state changes. 73 * 74 * @param playbackState notifies the updated player state. 75 */ onStateChanged(@layerState int playbackState)76 void onStateChanged(@PlayerState int playbackState); 77 78 /** Called when player has ended with an error. */ onError(Exception e)79 void onError(Exception e); 80 81 /** Called when size of input video to the player changes. */ onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio)82 void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio); 83 84 /** Called when player rendered its first frame. */ onRenderedFirstFrame()85 void onRenderedFirstFrame(); 86 87 /** Called when audio stream is unplayable. */ onAudioUnplayable()88 void onAudioUnplayable(); 89 90 /** Called when player drops some frames. */ onSmoothTrickplayForceStopped()91 void onSmoothTrickplayForceStopped(); 92 } 93 94 /** Interface definition for a callback to be notified of changes on video display. */ 95 public interface VideoEventListener { 96 /** Notifies the caption event. */ onEmitCaptionEvent(CaptionEvent event)97 void onEmitCaptionEvent(CaptionEvent event); 98 99 /** Notifies clearing up whole closed caption event. */ onClearCaptionEvent()100 void onClearCaptionEvent(); 101 102 /** Notifies the discovered caption service number. */ onDiscoverCaptionServiceNumber(int serviceNumber)103 void onDiscoverCaptionServiceNumber(int serviceNumber); 104 105 } 106 107 public static final int MIN_BUFFER_MS = 0; 108 public static final int MIN_REBUFFER_MS = 500; 109 110 @IntDef({TRACK_TYPE_VIDEO, TRACK_TYPE_AUDIO, TRACK_TYPE_TEXT}) 111 @Retention(RetentionPolicy.SOURCE) 112 public @interface TrackType {} 113 114 public static final int TRACK_TYPE_VIDEO = 0; 115 public static final int TRACK_TYPE_AUDIO = 1; 116 public static final int TRACK_TYPE_TEXT = 2; 117 118 @Retention(RetentionPolicy.SOURCE) 119 @IntDef({STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED}) 120 public @interface PlayerState {} 121 122 public static final int STATE_IDLE = Player.STATE_IDLE; 123 public static final int STATE_BUFFERING = Player.STATE_BUFFERING; 124 public static final int STATE_READY = Player.STATE_READY; 125 public static final int STATE_ENDED = Player.STATE_ENDED; 126 127 private static final float MAX_SMOOTH_TRICKPLAY_SPEED = 9.0f; 128 private static final float MIN_SMOOTH_TRICKPLAY_SPEED = 0.1f; 129 130 private int mCaptionServiceNumber = Cea708Data.EMPTY_SERVICE_NUMBER; 131 132 private final Context mContext; 133 private final SimpleExoPlayer mPlayer; 134 private final DefaultTrackSelector mTrackSelector; 135 private final TsDataSourceManager mSourceManager; 136 137 private DefaultTrackSelector.Parameters mTrackSelectorParameters; 138 private TrackGroupArray mLastSeenTrackGroupArray; 139 private Callback mCallback; 140 private TsDataSource mDataSource; 141 private VideoEventListener mVideoEventListener; 142 private boolean mTrickplayRunning; 143 144 /** 145 * Creates MPEG2-TS stream player. 146 * 147 * @param context the application context 148 * @param sourceManager the manager for {@link TsDataSource} 149 * @param callback callback for playback state changes 150 */ MpegTsPlayerV2(Context context, TsDataSourceManager sourceManager, Callback callback)151 public MpegTsPlayerV2(Context context, TsDataSourceManager sourceManager, Callback callback) { 152 mContext = context; 153 mTrackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build(); 154 mTrackSelector = new DefaultTrackSelector(); 155 mTrackSelector.setParameters(mTrackSelectorParameters); 156 mLastSeenTrackGroupArray = null; 157 mPlayer = ExoPlayerFactory.newSimpleInstance(context, mTrackSelector); 158 mPlayer.addListener(this); 159 mPlayer.addVideoListener(this); 160 mPlayer.addAudioListener(this); 161 mPlayer.addTextOutput(this); 162 mSourceManager = sourceManager; 163 mCallback = callback; 164 } 165 166 /** 167 * Sets the video event listener. 168 * 169 * @param videoEventListener the listener for video events 170 */ setVideoEventListener(VideoEventListener videoEventListener)171 public void setVideoEventListener(VideoEventListener videoEventListener) { 172 mVideoEventListener = videoEventListener; 173 } 174 175 /** 176 * Sets the closed caption service number. 177 * 178 * @param captionServiceNumber the service number of CEA-708 closed caption 179 */ setCaptionServiceNumber(int captionServiceNumber)180 public void setCaptionServiceNumber(int captionServiceNumber) { 181 mCaptionServiceNumber = captionServiceNumber; 182 if (captionServiceNumber == Cea708Data.EMPTY_SERVICE_NUMBER) return; 183 MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo(); 184 if (mappedTrackInfo != null) { 185 int rendererCount = mappedTrackInfo.getRendererCount(); 186 for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { 187 if (mappedTrackInfo.getRendererType(rendererIndex) == C.TRACK_TYPE_TEXT) { 188 TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); 189 for (int i = 0; i < trackGroupArray.length; i++) { 190 int readServiceNumber = 191 trackGroupArray.get(i).getFormat(0).accessibilityChannel; 192 int serviceNumber = 193 readServiceNumber == Format.NO_VALUE ? 1 : readServiceNumber; 194 if (serviceNumber == captionServiceNumber) { 195 setSelectedTrack(TRACK_TYPE_TEXT, i); 196 } 197 } 198 } 199 } 200 } 201 } 202 203 /** 204 * Invoked each time there is a change in the {@link Cue}s to be rendered 205 * 206 * @param cues The {@link Cue}s to be rendered, or an empty list if no cues are to be rendered. 207 */ 208 @Override onCues(List<Cue> cues)209 public void onCues(List<Cue> cues) { 210 mVideoEventListener.onEmitCaptionEvent( 211 new CaptionEvent( 212 Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DFX, 213 new Cea708Data.CaptionWindow( 214 /* id= */ 0, 215 /* visible= */ true, 216 /* rowlock= */ false, 217 /* columnLock= */ false, 218 /* priority= */ 3, 219 /* relativePositioning= */ true, 220 /* anchorVertical= */ 0, 221 /* anchorHorizontal= */ 0, 222 /* anchorId= */ 0, 223 /* rowCount= */ 0, 224 /* columnCount= */ 0, 225 /* penStyle= */ 0, 226 /* windowStyle= */ 2))); 227 mVideoEventListener.onEmitCaptionEvent( 228 new CaptionEvent(Cea708Parser.CAPTION_EMIT_TYPE_BUFFER, 229 cues)); 230 } 231 232 /** 233 * Sets the surface for the player. 234 * 235 * @param surface the {@link Surface} to render video 236 */ setSurface(Surface surface)237 public void setSurface(Surface surface) { 238 mPlayer.setVideoSurface(surface); 239 } 240 241 /** 242 * Creates renderers and {@link TsDataSource} and initializes player. 243 * 244 * @return true when everything is created and initialized well, false otherwise 245 */ prepare(TunerChannel channel, EventDetector.EventListener eventListener)246 public boolean prepare(TunerChannel channel, EventDetector.EventListener eventListener) { 247 TsDataSource source = null; 248 if (channel != null) { 249 source = mSourceManager.createDataSource(mContext, channel, eventListener); 250 if (source == null) { 251 return false; 252 } 253 } 254 mDataSource = source; 255 MediaSource mediaSource = 256 new ProgressiveMediaSource.Factory(() -> mDataSource).createMediaSource(Uri.EMPTY); 257 mPlayer.prepare(mediaSource, true, false); 258 return true; 259 } 260 261 262 /** Returns {@link TsDataSource} which provides MPEG2-TS stream. */ getDataSource()263 public TsDataSource getDataSource() { 264 return mDataSource; 265 } 266 267 /** 268 * Sets the player state to pause or play. 269 * 270 * @param playWhenReady sets the player state to being ready to play when {@code true}, sets the 271 * player state to being paused when {@code false} 272 */ setPlayWhenReady(boolean playWhenReady)273 public void setPlayWhenReady(boolean playWhenReady) { 274 mPlayer.setPlayWhenReady(playWhenReady); 275 stopSmoothTrickplay(false); 276 } 277 278 /** Returns true, if trickplay is supported. */ supportSmoothTrickPlay(float playbackSpeed)279 public boolean supportSmoothTrickPlay(float playbackSpeed) { 280 return playbackSpeed > MIN_SMOOTH_TRICKPLAY_SPEED 281 && playbackSpeed < MAX_SMOOTH_TRICKPLAY_SPEED; 282 } 283 284 /** 285 * Starts trickplay. It'll be reset, if {@link #seekTo} or {@link #setPlayWhenReady} is called. 286 */ startSmoothTrickplay(PlaybackParams playbackParams)287 public void startSmoothTrickplay(PlaybackParams playbackParams) { 288 SoftPreconditions.checkState(supportSmoothTrickPlay(playbackParams.getSpeed())); 289 mPlayer.setPlayWhenReady(true); 290 mTrickplayRunning = true; 291 } 292 stopSmoothTrickplay(boolean calledBySeek)293 private void stopSmoothTrickplay(boolean calledBySeek) { 294 if (mTrickplayRunning) { 295 mTrickplayRunning = false; 296 } 297 } 298 299 /** 300 * Seeks to the specified position of the current playback. 301 * 302 * @param positionMs the specified position in milli seconds. 303 */ seekTo(long positionMs)304 public void seekTo(long positionMs) { 305 mPlayer.seekTo(positionMs); 306 stopSmoothTrickplay(true); 307 } 308 309 /** Releases the player. */ release()310 public void release() { 311 if (mDataSource != null) { 312 mDataSource = null; 313 } 314 mCallback = null; 315 mPlayer.release(); 316 } 317 318 /** Returns the current status of the player. */ getPlaybackState()319 public int getPlaybackState() { 320 return mPlayer.getPlaybackState(); 321 } 322 323 /** Returns {@code true} when the player is prepared to play, {@code false} otherwise. */ isPrepared()324 public boolean isPrepared() { 325 int state = getPlaybackState(); 326 return state == ExoPlayer.STATE_READY || state == ExoPlayer.STATE_BUFFERING; 327 } 328 329 /** Returns {@code true} when the player is being ready to play, {@code false} otherwise. */ isPlaying()330 public boolean isPlaying() { 331 int state = getPlaybackState(); 332 return (state == ExoPlayer.STATE_READY || state == ExoPlayer.STATE_BUFFERING) 333 && mPlayer.getPlayWhenReady(); 334 } 335 336 /** Returns {@code true} when the player is buffering, {@code false} otherwise. */ isBuffering()337 public boolean isBuffering() { 338 return getPlaybackState() == ExoPlayer.STATE_BUFFERING; 339 } 340 341 /** Returns the current position of the playback in milli seconds. */ getCurrentPosition()342 public long getCurrentPosition() { 343 return mPlayer.getCurrentPosition(); 344 } 345 346 /** 347 * Sets the volume of the audio. 348 * 349 * @param volume see also 350 * {@link com.google.android.exoplayer2.Player.AudioComponent#setVolume(float)} 351 */ setVolume(float volume)352 public void setVolume(float volume) { 353 mPlayer.setVolume(volume); 354 } 355 356 /** 357 * Enables or disables audio and closed caption. 358 * 359 * @param enable enables the audio and closed caption when {@code true}, disables otherwise. 360 */ setAudioTrackAndClosedCaption(boolean enable)361 public void setAudioTrackAndClosedCaption(boolean enable) {} 362 363 @Override onTimelineChanged( Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason)364 public void onTimelineChanged( 365 Timeline timeline, 366 @Nullable Object manifest, 367 @Player.TimelineChangeReason int reason) {} 368 369 @Override onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections)370 public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { 371 if (trackGroups != mLastSeenTrackGroupArray) { 372 mLastSeenTrackGroupArray = trackGroups; 373 } 374 if (mVideoEventListener != null) { 375 MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo(); 376 if (mappedTrackInfo != null) { 377 int rendererCount = mappedTrackInfo.getRendererCount(); 378 for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { 379 if (mappedTrackInfo.getRendererType(rendererIndex) == C.TRACK_TYPE_TEXT) { 380 TrackGroupArray trackGroupArray = 381 mappedTrackInfo.getTrackGroups(rendererIndex); 382 for (int i = 0; i < trackGroupArray.length; i++) { 383 int serviceNumber = 384 trackGroupArray.get(i).getFormat(0).accessibilityChannel; 385 mVideoEventListener.onDiscoverCaptionServiceNumber( 386 serviceNumber == Format.NO_VALUE ? 1 : serviceNumber); 387 } 388 } 389 } 390 } 391 } 392 } 393 394 /** 395 * Checks the stream for the renderer of required track type. 396 * 397 * @param trackType Returns {@code true} if the player has any renderer for track type 398 * {@trackType}, {@code false} otherwise. 399 */ hasRendererType(int trackType)400 private boolean hasRendererType(int trackType) { 401 MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo(); 402 if (mappedTrackInfo != null) { 403 int rendererCount = mappedTrackInfo.getRendererCount(); 404 for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { 405 if (mappedTrackInfo.getRendererType(rendererIndex) == trackType) { 406 return true; 407 } 408 } 409 } 410 return false; 411 } 412 413 /** Returns {@code true} if the player has any video track, {@code false} otherwise. */ hasVideo()414 public boolean hasVideo() { 415 return hasRendererType(C.TRACK_TYPE_VIDEO); 416 } 417 418 /** Returns {@code true} if the player has any audio track, {@code false} otherwise. */ hasAudio()419 public boolean hasAudio() { 420 return hasRendererType(C.TRACK_TYPE_AUDIO); 421 } 422 423 /** Returns the number of tracks exposed by the specified renderer. */ getTrackCount(int rendererIndex)424 public int getTrackCount(int rendererIndex) { 425 MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo(); 426 if (mappedTrackInfo != null) { 427 TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); 428 return trackGroupArray.length; 429 } 430 return 0; 431 } 432 433 /** Selects a track for the specified renderer. */ setSelectedTrack(int rendererIndex, int trackIndex)434 public void setSelectedTrack(int rendererIndex, int trackIndex) { 435 MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo(); 436 if (trackIndex >= getTrackCount(rendererIndex)) { 437 return; 438 } 439 TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); 440 mTrackSelectorParameters = mTrackSelector.getParameters(); 441 DefaultTrackSelector.SelectionOverride override = 442 new DefaultTrackSelector.SelectionOverride(trackIndex, 0); 443 DefaultTrackSelector.ParametersBuilder builder = 444 mTrackSelectorParameters.buildUpon() 445 .clearSelectionOverrides(rendererIndex) 446 .setRendererDisabled(rendererIndex, false) 447 .setSelectionOverride(rendererIndex, trackGroupArray, override); 448 mTrackSelector.setParameters(builder); 449 } 450 451 /** 452 * Returns the index of the currently selected track for the specified renderer. 453 * 454 * @param rendererIndex The index of the renderer. 455 * @return The selected track. A negative value or a value greater than or equal to the 456 * renderer's track count indicates that the renderer is disabled. 457 */ getSelectedTrack(int rendererIndex)458 public int getSelectedTrack(int rendererIndex) { 459 TrackSelection trackSelection = mPlayer.getCurrentTrackSelections().get(rendererIndex); 460 MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo(); 461 TrackGroupArray trackGroupArray; 462 if (trackSelection != null && mappedTrackInfo != null) { 463 trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); 464 return trackGroupArray.indexOf(trackSelection.getTrackGroup()); 465 } 466 return C.INDEX_UNSET; 467 } 468 469 /** 470 * Returns the format of a track. 471 * 472 * @param rendererIndex The index of the renderer. 473 * @param trackIndex The index of the track. 474 * @return The format of the track. 475 */ getTrackFormat(int rendererIndex, int trackIndex)476 public Format getTrackFormat(int rendererIndex, int trackIndex) { 477 MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo(); 478 if (mappedTrackInfo != null) { 479 TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); 480 TrackGroup trackGroup = trackGroupArray.get(trackIndex); 481 return trackGroup.getFormat(0); 482 } 483 return null; 484 } 485 486 @Override onPlayerStateChanged(boolean playWhenReady, @PlayerState int state)487 public void onPlayerStateChanged(boolean playWhenReady, @PlayerState int state) { 488 if (mCallback == null) { 489 return; 490 } 491 mCallback.onStateChanged(state); 492 if (state == STATE_READY && hasVideo() && playWhenReady) { 493 Format format = mPlayer.getVideoFormat(); 494 mCallback.onVideoSizeChanged(format.width, format.height, format.pixelWidthHeightRatio); 495 } 496 } 497 498 @Override onPlayerError(ExoPlaybackException exception)499 public void onPlayerError(ExoPlaybackException exception) { 500 if (mCallback != null) { 501 mCallback.onError(exception); 502 } 503 } 504 505 @Override onVideoSizeChanged( int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio)506 public void onVideoSizeChanged( 507 int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { 508 if (mCallback != null) { 509 mCallback.onVideoSizeChanged(width, height, pixelWidthHeightRatio); 510 } 511 } 512 513 @Override onSurfaceSizeChanged(int width, int height)514 public void onSurfaceSizeChanged(int width, int height) {} 515 516 @Override onRenderedFirstFrame()517 public void onRenderedFirstFrame() { 518 if (mCallback != null) { 519 mCallback.onRenderedFirstFrame(); 520 } 521 } 522 523 @Override onDroppedFrames(int count, long elapsed)524 public void onDroppedFrames(int count, long elapsed) { 525 TunerDebug.notifyVideoFrameDrop(count, elapsed); 526 if (mTrickplayRunning && mCallback != null) { 527 mCallback.onSmoothTrickplayForceStopped(); 528 } 529 } 530 } 531