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