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