1 /* 2 * Copyright (C) 2015 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.exoplayer; 18 19 import android.content.Context; 20 import android.media.AudioFormat; 21 import android.media.MediaCodec.CryptoException; 22 import android.media.PlaybackParams; 23 import android.os.Handler; 24 import android.support.annotation.IntDef; 25 import android.view.Surface; 26 27 import com.google.android.exoplayer.DummyTrackRenderer; 28 import com.google.android.exoplayer.ExoPlaybackException; 29 import com.google.android.exoplayer.ExoPlayer; 30 import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; 31 import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; 32 import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; 33 import com.google.android.exoplayer.MediaFormat; 34 import com.google.android.exoplayer.TrackRenderer; 35 import com.google.android.exoplayer.audio.AudioCapabilities; 36 import com.google.android.exoplayer.audio.AudioTrack; 37 import com.google.android.exoplayer.upstream.DataSource; 38 import com.android.tv.common.SoftPreconditions; 39 import com.android.tv.tuner.data.Cea708Data; 40 import com.android.tv.tuner.data.Cea708Data.CaptionEvent; 41 import com.android.tv.tuner.data.TunerChannel; 42 import com.android.tv.tuner.exoplayer.audio.MpegTsDefaultAudioTrackRenderer; 43 import com.android.tv.tuner.exoplayer.audio.MpegTsMediaCodecAudioTrackRenderer; 44 import com.android.tv.tuner.source.TsDataSource; 45 import com.android.tv.tuner.source.TsDataSourceManager; 46 import com.android.tv.tuner.tvinput.EventDetector; 47 import com.android.tv.tuner.tvinput.TunerDebug; 48 49 import java.lang.annotation.Retention; 50 import java.lang.annotation.RetentionPolicy; 51 52 /** MPEG-2 TS stream player implementation using ExoPlayer. */ 53 public class MpegTsPlayer 54 implements ExoPlayer.Listener, 55 MediaCodecVideoTrackRenderer.EventListener, 56 MpegTsDefaultAudioTrackRenderer.EventListener, 57 MpegTsMediaCodecAudioTrackRenderer.Ac3EventListener { 58 private int mCaptionServiceNumber = Cea708Data.EMPTY_SERVICE_NUMBER; 59 60 /** 61 * Interface definition for building specific track renderers. 62 */ 63 public interface RendererBuilder { 64 void buildRenderers(MpegTsPlayer mpegTsPlayer, DataSource dataSource, 65 boolean hasSoftwareAudioDecoder, RendererBuilderCallback callback); 66 } 67 68 /** 69 * Interface definition for {@link RendererBuilder#buildRenderers} to notify the result. 70 */ 71 public interface RendererBuilderCallback { 72 void onRenderers(String[][] trackNames, TrackRenderer[] renderers); 73 void onRenderersError(Exception e); 74 } 75 76 /** 77 * Interface definition for a callback to be notified of changes in player state. 78 */ 79 public interface Listener { 80 void onStateChanged(boolean playWhenReady, int playbackState); 81 void onError(Exception e); 82 void onVideoSizeChanged(int width, int height, 83 float pixelWidthHeightRatio); 84 void onDrawnToSurface(MpegTsPlayer player, Surface surface); 85 void onAudioUnplayable(); 86 void onSmoothTrickplayForceStopped(); 87 } 88 89 /** 90 * Interface definition for a callback to be notified of changes on video display. 91 */ 92 public interface VideoEventListener { 93 /** 94 * Notifies the caption event. 95 */ 96 void onEmitCaptionEvent(CaptionEvent event); 97 98 /** 99 * Notifies clearing up whole closed caption event. 100 */ 101 void onClearCaptionEvent(); 102 103 /** 104 * Notifies the discovered caption service number. 105 */ 106 void onDiscoverCaptionServiceNumber(int serviceNumber); 107 } 108 109 public static final int RENDERER_COUNT = 3; 110 public static final int MIN_BUFFER_MS = 0; 111 public static final int MIN_REBUFFER_MS = 500; 112 113 @IntDef({TRACK_TYPE_VIDEO, TRACK_TYPE_AUDIO, TRACK_TYPE_TEXT}) 114 @Retention(RetentionPolicy.SOURCE) 115 public @interface TrackType {} 116 public static final int TRACK_TYPE_VIDEO = 0; 117 public static final int TRACK_TYPE_AUDIO = 1; 118 public static final int TRACK_TYPE_TEXT = 2; 119 120 @IntDef({RENDERER_BUILDING_STATE_IDLE, RENDERER_BUILDING_STATE_BUILDING, 121 RENDERER_BUILDING_STATE_BUILT}) 122 @Retention(RetentionPolicy.SOURCE) 123 public @interface RendererBuildingState {} 124 private static final int RENDERER_BUILDING_STATE_IDLE = 1; 125 private static final int RENDERER_BUILDING_STATE_BUILDING = 2; 126 private static final int RENDERER_BUILDING_STATE_BUILT = 3; 127 128 private static final float MAX_SMOOTH_TRICKPLAY_SPEED = 9.0f; 129 private static final float MIN_SMOOTH_TRICKPLAY_SPEED = 0.1f; 130 131 private final RendererBuilder mRendererBuilder; 132 private final ExoPlayer mPlayer; 133 private final Handler mMainHandler; 134 private final AudioCapabilities mAudioCapabilities; 135 private final TsDataSourceManager mSourceManager; 136 137 private Listener mListener; 138 @RendererBuildingState private int mRendererBuildingState; 139 140 private Surface mSurface; 141 private TsDataSource mDataSource; 142 private InternalRendererBuilderCallback mBuilderCallback; 143 private TrackRenderer mVideoRenderer; 144 private TrackRenderer mAudioRenderer; 145 private Cea708TextTrackRenderer mTextRenderer; 146 private final Cea708TextTrackRenderer.CcListener mCcListener; 147 private VideoEventListener mVideoEventListener; 148 private boolean mTrickplayRunning; 149 private float mVolume; 150 151 /** 152 * Creates MPEG2-TS stream player. 153 * 154 * @param rendererBuilder the builder of track renderers 155 * @param handler the handler for the playback events in track renderers 156 * @param sourceManager the manager for {@link DataSource} 157 * @param capabilities the {@link AudioCapabilities} of the current device 158 * @param listener the listener for playback state changes 159 */ 160 public MpegTsPlayer(RendererBuilder rendererBuilder, Handler handler, 161 TsDataSourceManager sourceManager, AudioCapabilities capabilities, 162 Listener listener) { 163 mRendererBuilder = rendererBuilder; 164 mPlayer = ExoPlayer.Factory.newInstance(RENDERER_COUNT, MIN_BUFFER_MS, MIN_REBUFFER_MS); 165 mPlayer.addListener(this); 166 mMainHandler = handler; 167 mAudioCapabilities = capabilities; 168 mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; 169 mCcListener = new MpegTsCcListener(); 170 mSourceManager = sourceManager; 171 mListener = listener; 172 } 173 174 /** 175 * Sets the video event listener. 176 * 177 * @param videoEventListener the listener for video events 178 */ 179 public void setVideoEventListener(VideoEventListener videoEventListener) { 180 mVideoEventListener = videoEventListener; 181 } 182 183 /** 184 * Sets the closed caption service number. 185 * 186 * @param captionServiceNumber the service number of CEA-708 closed caption 187 */ 188 public void setCaptionServiceNumber(int captionServiceNumber) { 189 mCaptionServiceNumber = captionServiceNumber; 190 if (mTextRenderer != null) { 191 mPlayer.sendMessage(mTextRenderer, 192 Cea708TextTrackRenderer.MSG_SERVICE_NUMBER, mCaptionServiceNumber); 193 } 194 } 195 196 /** 197 * Sets the surface for the player. 198 * 199 * @param surface the {@link Surface} to render video 200 */ 201 public void setSurface(Surface surface) { 202 mSurface = surface; 203 pushSurface(false); 204 } 205 206 /** 207 * Returns the current surface of the player. 208 */ 209 public Surface getSurface() { 210 return mSurface; 211 } 212 213 /** 214 * Clears the surface and waits until the surface is being cleaned. 215 */ 216 public void blockingClearSurface() { 217 mSurface = null; 218 pushSurface(true); 219 } 220 221 /** 222 * Creates renderers and {@link DataSource} and initializes player. 223 * @param context a {@link Context} instance 224 * @param channel to play 225 * @param hasSoftwareAudioDecoder {@code true} if there is connected software decoder 226 * @param eventListener for program information which will be scanned from MPEG2-TS stream 227 * @return true when everything is created and initialized well, false otherwise 228 */ 229 public boolean prepare(Context context, TunerChannel channel, boolean hasSoftwareAudioDecoder, 230 EventDetector.EventListener eventListener) { 231 TsDataSource source = null; 232 if (channel != null) { 233 source = mSourceManager.createDataSource(context, channel, eventListener); 234 if (source == null) { 235 return false; 236 } 237 } 238 mDataSource = source; 239 if (mRendererBuildingState == RENDERER_BUILDING_STATE_BUILT) { 240 mPlayer.stop(); 241 } 242 if (mBuilderCallback != null) { 243 mBuilderCallback.cancel(); 244 } 245 mRendererBuildingState = RENDERER_BUILDING_STATE_BUILDING; 246 mBuilderCallback = new InternalRendererBuilderCallback(); 247 mRendererBuilder.buildRenderers(this, source, hasSoftwareAudioDecoder, mBuilderCallback); 248 return true; 249 } 250 251 /** 252 * Returns {@link TsDataSource} which provides MPEG2-TS stream. 253 */ 254 public TsDataSource getDataSource() { 255 return mDataSource; 256 } 257 258 private void onRenderers(TrackRenderer[] renderers) { 259 mBuilderCallback = null; 260 for (int i = 0; i < RENDERER_COUNT; i++) { 261 if (renderers[i] == null) { 262 // Convert a null renderer to a dummy renderer. 263 renderers[i] = new DummyTrackRenderer(); 264 } 265 } 266 mVideoRenderer = renderers[TRACK_TYPE_VIDEO]; 267 mAudioRenderer = renderers[TRACK_TYPE_AUDIO]; 268 mTextRenderer = (Cea708TextTrackRenderer) renderers[TRACK_TYPE_TEXT]; 269 mTextRenderer.setCcListener(mCcListener); 270 mPlayer.sendMessage( 271 mTextRenderer, Cea708TextTrackRenderer.MSG_SERVICE_NUMBER, mCaptionServiceNumber); 272 mRendererBuildingState = RENDERER_BUILDING_STATE_BUILT; 273 pushSurface(false); 274 mPlayer.prepare(renderers); 275 pushTrackSelection(TRACK_TYPE_VIDEO, true); 276 pushTrackSelection(TRACK_TYPE_AUDIO, true); 277 pushTrackSelection(TRACK_TYPE_TEXT, true); 278 } 279 280 private void onRenderersError(Exception e) { 281 mBuilderCallback = null; 282 mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; 283 if (mListener != null) { 284 mListener.onError(e); 285 } 286 } 287 288 /** 289 * Sets the player state to pause or play. 290 * 291 * @param playWhenReady sets the player state to being ready to play when {@code true}, 292 * sets the player state to being paused when {@code false} 293 * 294 */ 295 public void setPlayWhenReady(boolean playWhenReady) { 296 mPlayer.setPlayWhenReady(playWhenReady); 297 stopSmoothTrickplay(false); 298 } 299 300 /** 301 * Returns true, if trickplay is supported. 302 */ 303 public boolean supportSmoothTrickPlay(float playbackSpeed) { 304 return playbackSpeed > MIN_SMOOTH_TRICKPLAY_SPEED 305 && playbackSpeed < MAX_SMOOTH_TRICKPLAY_SPEED; 306 } 307 308 /** 309 * Starts trickplay. It'll be reset, if {@link #seekTo} or {@link #setPlayWhenReady} is called. 310 */ 311 public void startSmoothTrickplay(PlaybackParams playbackParams) { 312 SoftPreconditions.checkState(supportSmoothTrickPlay(playbackParams.getSpeed())); 313 mPlayer.setPlayWhenReady(true); 314 mTrickplayRunning = true; 315 if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { 316 mPlayer.sendMessage( 317 mAudioRenderer, 318 MpegTsDefaultAudioTrackRenderer.MSG_SET_PLAYBACK_SPEED, 319 playbackParams.getSpeed()); 320 } else { 321 mPlayer.sendMessage(mAudioRenderer, 322 MediaCodecAudioTrackRenderer.MSG_SET_PLAYBACK_PARAMS, 323 playbackParams); 324 } 325 } 326 327 private void stopSmoothTrickplay(boolean calledBySeek) { 328 if (mTrickplayRunning) { 329 mTrickplayRunning = false; 330 if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { 331 mPlayer.sendMessage( 332 mAudioRenderer, MpegTsDefaultAudioTrackRenderer.MSG_SET_PLAYBACK_SPEED, 333 1.0f); 334 } else { 335 mPlayer.sendMessage(mAudioRenderer, 336 MediaCodecAudioTrackRenderer.MSG_SET_PLAYBACK_PARAMS, 337 new PlaybackParams().setSpeed(1.0f)); 338 } 339 if (!calledBySeek) { 340 mPlayer.seekTo(mPlayer.getCurrentPosition()); 341 } 342 } 343 } 344 345 /** 346 * Seeks to the specified position of the current playback. 347 * 348 * @param positionMs the specified position in milli seconds. 349 */ 350 public void seekTo(long positionMs) { 351 mPlayer.seekTo(positionMs); 352 stopSmoothTrickplay(true); 353 } 354 355 /** 356 * Releases the player. 357 */ 358 public void release() { 359 if (mDataSource != null) { 360 mSourceManager.releaseDataSource(mDataSource); 361 mDataSource = null; 362 } 363 if (mBuilderCallback != null) { 364 mBuilderCallback.cancel(); 365 mBuilderCallback = null; 366 } 367 mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; 368 mSurface = null; 369 mListener = null; 370 mPlayer.release(); 371 } 372 373 /** 374 * Returns the current status of the player. 375 */ 376 public int getPlaybackState() { 377 if (mRendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) { 378 return ExoPlayer.STATE_PREPARING; 379 } 380 return mPlayer.getPlaybackState(); 381 } 382 383 /** 384 * Returns {@code true} when the player is prepared to play, {@code false} otherwise. 385 */ 386 public boolean isPrepared() { 387 int state = getPlaybackState(); 388 return state == ExoPlayer.STATE_READY || state == ExoPlayer.STATE_BUFFERING; 389 } 390 391 /** 392 * Returns {@code true} when the player is being ready to play, {@code false} otherwise. 393 */ 394 public boolean isPlaying() { 395 int state = getPlaybackState(); 396 return (state == ExoPlayer.STATE_READY || state == ExoPlayer.STATE_BUFFERING) 397 && mPlayer.getPlayWhenReady(); 398 } 399 400 /** 401 * Returns {@code true} when the player is buffering, {@code false} otherwise. 402 */ 403 public boolean isBuffering() { 404 return getPlaybackState() == ExoPlayer.STATE_BUFFERING; 405 } 406 407 /** 408 * Returns the current position of the playback in milli seconds. 409 */ 410 public long getCurrentPosition() { 411 return mPlayer.getCurrentPosition(); 412 } 413 414 /** 415 * Returns the total duration of the playback. 416 */ 417 public long getDuration() { 418 return mPlayer.getDuration(); 419 } 420 421 /** 422 * Returns {@code true} when the player is being ready to play, 423 * {@code false} when the player is paused. 424 */ 425 public boolean getPlayWhenReady() { 426 return mPlayer.getPlayWhenReady(); 427 } 428 429 /** 430 * Sets the volume of the audio. 431 * 432 * @param volume see also {@link AudioTrack#setVolume(float)} 433 */ 434 public void setVolume(float volume) { 435 mVolume = volume; 436 if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { 437 mPlayer.sendMessage(mAudioRenderer, MpegTsDefaultAudioTrackRenderer.MSG_SET_VOLUME, 438 volume); 439 } else { 440 mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, 441 volume); 442 } 443 } 444 445 /** 446 * Enables or disables audio and closed caption. 447 * 448 * @param enable enables the audio and closed caption when {@code true}, disables otherwise. 449 */ 450 public void setAudioTrackAndClosedCaption(boolean enable) { 451 if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { 452 mPlayer.sendMessage(mAudioRenderer, MpegTsDefaultAudioTrackRenderer.MSG_SET_AUDIO_TRACK, 453 enable ? 1 : 0); 454 } else { 455 mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, 456 enable ? mVolume : 0.0f); 457 } 458 mPlayer.sendMessage(mTextRenderer, Cea708TextTrackRenderer.MSG_ENABLE_CLOSED_CAPTION, 459 enable); 460 } 461 462 /** 463 * Returns {@code true} when AC3 audio can be played, {@code false} otherwise. 464 */ 465 public boolean isAc3Playable() { 466 return mAudioCapabilities != null 467 && mAudioCapabilities.supportsEncoding(AudioFormat.ENCODING_AC3); 468 } 469 470 /** 471 * Notifies when the audio cannot be played by the current device. 472 */ 473 public void onAudioUnplayable() { 474 if (mListener != null) { 475 mListener.onAudioUnplayable(); 476 } 477 } 478 479 /** 480 * Returns {@code true} if the player has any video track, {@code false} otherwise. 481 */ 482 public boolean hasVideo() { 483 return mPlayer.getTrackCount(TRACK_TYPE_VIDEO) > 0; 484 } 485 486 /** 487 * Returns {@code true} if the player has any audio trock, {@code false} otherwise. 488 */ 489 public boolean hasAudio() { 490 return mPlayer.getTrackCount(TRACK_TYPE_AUDIO) > 0; 491 } 492 493 /** 494 * Returns the number of tracks exposed by the specified renderer. 495 */ 496 public int getTrackCount(int rendererIndex) { 497 return mPlayer.getTrackCount(rendererIndex); 498 } 499 500 /** 501 * Selects a track for the specified renderer. 502 */ 503 public void setSelectedTrack(int rendererIndex, int trackIndex) { 504 if (trackIndex >= getTrackCount(rendererIndex)) { 505 return; 506 } 507 mPlayer.setSelectedTrack(rendererIndex, trackIndex); 508 } 509 510 /** 511 * Returns the index of the currently selected track for the specified renderer. 512 * 513 * @param rendererIndex The index of the renderer. 514 * @return The selected track. A negative value or a value greater than or equal to the renderer's 515 * track count indicates that the renderer is disabled. 516 */ 517 public int getSelectedTrack(int rendererIndex) { 518 return mPlayer.getSelectedTrack(rendererIndex); 519 } 520 521 /** 522 * Returns the format of a track. 523 * 524 * @param rendererIndex The index of the renderer. 525 * @param trackIndex The index of the track. 526 * @return The format of the track. 527 */ 528 public MediaFormat getTrackFormat(int rendererIndex, int trackIndex) { 529 return mPlayer.getTrackFormat(rendererIndex, trackIndex); 530 } 531 532 /** 533 * Gets the main handler of the player. 534 */ 535 /* package */ Handler getMainHandler() { 536 return mMainHandler; 537 } 538 539 @Override 540 public void onPlayerStateChanged(boolean playWhenReady, int state) { 541 if (mListener == null) { 542 return; 543 } 544 mListener.onStateChanged(playWhenReady, state); 545 if (state == ExoPlayer.STATE_READY && mPlayer.getTrackCount(TRACK_TYPE_VIDEO) > 0 546 && playWhenReady) { 547 MediaFormat format = mPlayer.getTrackFormat(TRACK_TYPE_VIDEO, 0); 548 mListener.onVideoSizeChanged(format.width, 549 format.height, format.pixelWidthHeightRatio); 550 } 551 } 552 553 @Override 554 public void onPlayerError(ExoPlaybackException exception) { 555 mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; 556 if (mListener != null) { 557 mListener.onError(exception); 558 } 559 } 560 561 @Override 562 public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, 563 float pixelWidthHeightRatio) { 564 if (mListener != null) { 565 mListener.onVideoSizeChanged(width, height, pixelWidthHeightRatio); 566 } 567 } 568 569 @Override 570 public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, 571 long initializationDurationMs) { 572 // Do nothing. 573 } 574 575 @Override 576 public void onDecoderInitializationError(DecoderInitializationException e) { 577 // Do nothing. 578 } 579 580 @Override 581 public void onAudioTrackInitializationError(AudioTrack.InitializationException e) { 582 if (mListener != null) { 583 mListener.onAudioUnplayable(); 584 } 585 } 586 587 @Override 588 public void onAudioTrackWriteError(AudioTrack.WriteException e) { 589 // Do nothing. 590 } 591 592 @Override 593 public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, 594 long elapsedSinceLastFeedMs) { 595 // Do nothing. 596 } 597 598 @Override 599 public void onCryptoError(CryptoException e) { 600 // Do nothing. 601 } 602 603 @Override 604 public void onPlayWhenReadyCommitted() { 605 // Do nothing. 606 } 607 608 @Override 609 public void onDrawnToSurface(Surface surface) { 610 if (mListener != null) { 611 mListener.onDrawnToSurface(this, surface); 612 } 613 } 614 615 @Override 616 public void onDroppedFrames(int count, long elapsed) { 617 TunerDebug.notifyVideoFrameDrop(count, elapsed); 618 if (mTrickplayRunning && mListener != null) { 619 mListener.onSmoothTrickplayForceStopped(); 620 } 621 } 622 623 @Override 624 public void onAudioTrackSetPlaybackParamsError(IllegalArgumentException e) { 625 if (mTrickplayRunning && mListener != null) { 626 mListener.onSmoothTrickplayForceStopped(); 627 } 628 } 629 630 private void pushSurface(boolean blockForSurfacePush) { 631 if (mRendererBuildingState != RENDERER_BUILDING_STATE_BUILT) { 632 return; 633 } 634 635 if (blockForSurfacePush) { 636 mPlayer.blockingSendMessage( 637 mVideoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, mSurface); 638 } else { 639 mPlayer.sendMessage( 640 mVideoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, mSurface); 641 } 642 } 643 644 private void pushTrackSelection(@TrackType int type, boolean allowRendererEnable) { 645 if (mRendererBuildingState != RENDERER_BUILDING_STATE_BUILT) { 646 return; 647 } 648 mPlayer.setSelectedTrack(type, allowRendererEnable ? 0 : -1); 649 } 650 651 private class MpegTsCcListener implements Cea708TextTrackRenderer.CcListener { 652 653 @Override 654 public void emitEvent(CaptionEvent captionEvent) { 655 if (mVideoEventListener != null) { 656 mVideoEventListener.onEmitCaptionEvent(captionEvent); 657 } 658 } 659 660 @Override 661 public void clearCaption() { 662 if (mVideoEventListener != null) { 663 mVideoEventListener.onClearCaptionEvent(); 664 } 665 } 666 667 @Override 668 public void discoverServiceNumber(int serviceNumber) { 669 if (mVideoEventListener != null) { 670 mVideoEventListener.onDiscoverCaptionServiceNumber(serviceNumber); 671 } 672 } 673 } 674 675 private class InternalRendererBuilderCallback implements RendererBuilderCallback { 676 private boolean canceled; 677 678 public void cancel() { 679 canceled = true; 680 } 681 682 @Override 683 public void onRenderers(String[][] trackNames, TrackRenderer[] renderers) { 684 if (!canceled) { 685 MpegTsPlayer.this.onRenderers(renderers); 686 } 687 } 688 689 @Override 690 public void onRenderersError(Exception e) { 691 if (!canceled) { 692 MpegTsPlayer.this.onRenderersError(e); 693 } 694 } 695 } 696 }