diff options
Diffstat (limited to 'tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java')
-rw-r--r-- | tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java | 672 |
1 files changed, 672 insertions, 0 deletions
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java new file mode 100644 index 00000000..a49cbfaf --- /dev/null +++ b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java @@ -0,0 +1,672 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.tuner.exoplayer; + +import android.content.Context; +import android.media.AudioFormat; +import android.media.MediaCodec.CryptoException; +import android.media.PlaybackParams; +import android.os.Handler; +import android.support.annotation.IntDef; +import android.view.Surface; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.tuner.data.Cea708Data; +import com.android.tv.tuner.data.Cea708Data.CaptionEvent; +import com.android.tv.tuner.data.TunerChannel; +import com.android.tv.tuner.exoplayer.audio.MpegTsDefaultAudioTrackRenderer; +import com.android.tv.tuner.exoplayer.audio.MpegTsMediaCodecAudioTrackRenderer; +import com.android.tv.tuner.source.TsDataSource; +import com.android.tv.tuner.source.TsDataSourceManager; +import com.android.tv.tuner.tvinput.EventDetector; +import com.android.tv.tuner.tvinput.TunerDebug; +import com.google.android.exoplayer.DummyTrackRenderer; +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.ExoPlayer; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.audio.AudioCapabilities; +import com.google.android.exoplayer.audio.AudioTrack; +import com.google.android.exoplayer.upstream.DataSource; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** MPEG-2 TS stream player implementation using ExoPlayer. */ +public class MpegTsPlayer + implements ExoPlayer.Listener, + MediaCodecVideoTrackRenderer.EventListener, + MpegTsDefaultAudioTrackRenderer.EventListener, + MpegTsMediaCodecAudioTrackRenderer.Ac3EventListener { + private int mCaptionServiceNumber = Cea708Data.EMPTY_SERVICE_NUMBER; + + /** Interface definition for building specific track renderers. */ + public interface RendererBuilder { + void buildRenderers( + MpegTsPlayer mpegTsPlayer, + DataSource dataSource, + boolean hasSoftwareAudioDecoder, + RendererBuilderCallback callback); + } + + /** Interface definition for {@link RendererBuilder#buildRenderers} to notify the result. */ + public interface RendererBuilderCallback { + void onRenderers(String[][] trackNames, TrackRenderer[] renderers); + + void onRenderersError(Exception e); + } + + /** Interface definition for a callback to be notified of changes in player state. */ + public interface Listener { + void onStateChanged(boolean playWhenReady, int playbackState); + + void onError(Exception e); + + void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio); + + void onDrawnToSurface(MpegTsPlayer player, Surface surface); + + void onAudioUnplayable(); + + void onSmoothTrickplayForceStopped(); + } + + /** Interface definition for a callback to be notified of changes on video display. */ + public interface VideoEventListener { + /** Notifies the caption event. */ + void onEmitCaptionEvent(CaptionEvent event); + + /** Notifies clearing up whole closed caption event. */ + void onClearCaptionEvent(); + + /** Notifies the discovered caption service number. */ + void onDiscoverCaptionServiceNumber(int serviceNumber); + } + + public static final int RENDERER_COUNT = 3; + public static final int MIN_BUFFER_MS = 0; + public static final int MIN_REBUFFER_MS = 500; + + @IntDef({TRACK_TYPE_VIDEO, TRACK_TYPE_AUDIO, TRACK_TYPE_TEXT}) + @Retention(RetentionPolicy.SOURCE) + public @interface TrackType {} + + public static final int TRACK_TYPE_VIDEO = 0; + public static final int TRACK_TYPE_AUDIO = 1; + public static final int TRACK_TYPE_TEXT = 2; + + @IntDef({ + RENDERER_BUILDING_STATE_IDLE, + RENDERER_BUILDING_STATE_BUILDING, + RENDERER_BUILDING_STATE_BUILT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface RendererBuildingState {} + + private static final int RENDERER_BUILDING_STATE_IDLE = 1; + private static final int RENDERER_BUILDING_STATE_BUILDING = 2; + private static final int RENDERER_BUILDING_STATE_BUILT = 3; + + private static final float MAX_SMOOTH_TRICKPLAY_SPEED = 9.0f; + private static final float MIN_SMOOTH_TRICKPLAY_SPEED = 0.1f; + + private final RendererBuilder mRendererBuilder; + private final ExoPlayer mPlayer; + private final Handler mMainHandler; + private final AudioCapabilities mAudioCapabilities; + private final TsDataSourceManager mSourceManager; + + private Listener mListener; + @RendererBuildingState private int mRendererBuildingState; + + private Surface mSurface; + private TsDataSource mDataSource; + private InternalRendererBuilderCallback mBuilderCallback; + private TrackRenderer mVideoRenderer; + private TrackRenderer mAudioRenderer; + private Cea708TextTrackRenderer mTextRenderer; + private final Cea708TextTrackRenderer.CcListener mCcListener; + private VideoEventListener mVideoEventListener; + private boolean mTrickplayRunning; + private float mVolume; + + /** + * Creates MPEG2-TS stream player. + * + * @param rendererBuilder the builder of track renderers + * @param handler the handler for the playback events in track renderers + * @param sourceManager the manager for {@link DataSource} + * @param capabilities the {@link AudioCapabilities} of the current device + * @param listener the listener for playback state changes + */ + public MpegTsPlayer( + RendererBuilder rendererBuilder, + Handler handler, + TsDataSourceManager sourceManager, + AudioCapabilities capabilities, + Listener listener) { + mRendererBuilder = rendererBuilder; + mPlayer = ExoPlayer.Factory.newInstance(RENDERER_COUNT, MIN_BUFFER_MS, MIN_REBUFFER_MS); + mPlayer.addListener(this); + mMainHandler = handler; + mAudioCapabilities = capabilities; + mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + mCcListener = new MpegTsCcListener(); + mSourceManager = sourceManager; + mListener = listener; + } + + /** + * Sets the video event listener. + * + * @param videoEventListener the listener for video events + */ + public void setVideoEventListener(VideoEventListener videoEventListener) { + mVideoEventListener = videoEventListener; + } + + /** + * Sets the closed caption service number. + * + * @param captionServiceNumber the service number of CEA-708 closed caption + */ + public void setCaptionServiceNumber(int captionServiceNumber) { + mCaptionServiceNumber = captionServiceNumber; + if (mTextRenderer != null) { + mPlayer.sendMessage( + mTextRenderer, + Cea708TextTrackRenderer.MSG_SERVICE_NUMBER, + mCaptionServiceNumber); + } + } + + /** + * Sets the surface for the player. + * + * @param surface the {@link Surface} to render video + */ + public void setSurface(Surface surface) { + mSurface = surface; + pushSurface(false); + } + + /** Returns the current surface of the player. */ + public Surface getSurface() { + return mSurface; + } + + /** Clears the surface and waits until the surface is being cleaned. */ + public void blockingClearSurface() { + mSurface = null; + pushSurface(true); + } + + /** + * Creates renderers and {@link DataSource} and initializes player. + * + * @param context a {@link Context} instance + * @param channel to play + * @param hasSoftwareAudioDecoder {@code true} if there is connected software decoder + * @param eventListener for program information which will be scanned from MPEG2-TS stream + * @return true when everything is created and initialized well, false otherwise + */ + public boolean prepare( + Context context, + TunerChannel channel, + boolean hasSoftwareAudioDecoder, + EventDetector.EventListener eventListener) { + TsDataSource source = null; + if (channel != null) { + source = mSourceManager.createDataSource(context, channel, eventListener); + if (source == null) { + return false; + } + } + mDataSource = source; + if (mRendererBuildingState == RENDERER_BUILDING_STATE_BUILT) { + mPlayer.stop(); + } + if (mBuilderCallback != null) { + mBuilderCallback.cancel(); + } + mRendererBuildingState = RENDERER_BUILDING_STATE_BUILDING; + mBuilderCallback = new InternalRendererBuilderCallback(); + mRendererBuilder.buildRenderers(this, source, hasSoftwareAudioDecoder, mBuilderCallback); + return true; + } + + /** Returns {@link TsDataSource} which provides MPEG2-TS stream. */ + public TsDataSource getDataSource() { + return mDataSource; + } + + private void onRenderers(TrackRenderer[] renderers) { + mBuilderCallback = null; + for (int i = 0; i < RENDERER_COUNT; i++) { + if (renderers[i] == null) { + // Convert a null renderer to a dummy renderer. + renderers[i] = new DummyTrackRenderer(); + } + } + mVideoRenderer = renderers[TRACK_TYPE_VIDEO]; + mAudioRenderer = renderers[TRACK_TYPE_AUDIO]; + mTextRenderer = (Cea708TextTrackRenderer) renderers[TRACK_TYPE_TEXT]; + mTextRenderer.setCcListener(mCcListener); + mPlayer.sendMessage( + mTextRenderer, Cea708TextTrackRenderer.MSG_SERVICE_NUMBER, mCaptionServiceNumber); + mRendererBuildingState = RENDERER_BUILDING_STATE_BUILT; + pushSurface(false); + mPlayer.prepare(renderers); + pushTrackSelection(TRACK_TYPE_VIDEO, true); + pushTrackSelection(TRACK_TYPE_AUDIO, true); + pushTrackSelection(TRACK_TYPE_TEXT, true); + } + + private void onRenderersError(Exception e) { + mBuilderCallback = null; + mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + if (mListener != null) { + mListener.onError(e); + } + } + + /** + * Sets the player state to pause or play. + * + * @param playWhenReady sets the player state to being ready to play when {@code true}, sets the + * player state to being paused when {@code false} + */ + public void setPlayWhenReady(boolean playWhenReady) { + mPlayer.setPlayWhenReady(playWhenReady); + stopSmoothTrickplay(false); + } + + /** Returns true, if trickplay is supported. */ + public boolean supportSmoothTrickPlay(float playbackSpeed) { + return playbackSpeed > MIN_SMOOTH_TRICKPLAY_SPEED + && playbackSpeed < MAX_SMOOTH_TRICKPLAY_SPEED; + } + + /** + * Starts trickplay. It'll be reset, if {@link #seekTo} or {@link #setPlayWhenReady} is called. + */ + public void startSmoothTrickplay(PlaybackParams playbackParams) { + SoftPreconditions.checkState(supportSmoothTrickPlay(playbackParams.getSpeed())); + mPlayer.setPlayWhenReady(true); + mTrickplayRunning = true; + if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { + mPlayer.sendMessage( + mAudioRenderer, + MpegTsDefaultAudioTrackRenderer.MSG_SET_PLAYBACK_SPEED, + playbackParams.getSpeed()); + } else { + mPlayer.sendMessage( + mAudioRenderer, + MediaCodecAudioTrackRenderer.MSG_SET_PLAYBACK_PARAMS, + playbackParams); + } + } + + private void stopSmoothTrickplay(boolean calledBySeek) { + if (mTrickplayRunning) { + mTrickplayRunning = false; + if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { + mPlayer.sendMessage( + mAudioRenderer, + MpegTsDefaultAudioTrackRenderer.MSG_SET_PLAYBACK_SPEED, + 1.0f); + } else { + mPlayer.sendMessage( + mAudioRenderer, + MediaCodecAudioTrackRenderer.MSG_SET_PLAYBACK_PARAMS, + new PlaybackParams().setSpeed(1.0f)); + } + if (!calledBySeek) { + mPlayer.seekTo(mPlayer.getCurrentPosition()); + } + } + } + + /** + * Seeks to the specified position of the current playback. + * + * @param positionMs the specified position in milli seconds. + */ + public void seekTo(long positionMs) { + mPlayer.seekTo(positionMs); + stopSmoothTrickplay(true); + } + + /** Releases the player. */ + public void release() { + if (mDataSource != null) { + mSourceManager.releaseDataSource(mDataSource); + mDataSource = null; + } + if (mBuilderCallback != null) { + mBuilderCallback.cancel(); + mBuilderCallback = null; + } + mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + mSurface = null; + mListener = null; + mPlayer.release(); + } + + /** Returns the current status of the player. */ + public int getPlaybackState() { + if (mRendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) { + return ExoPlayer.STATE_PREPARING; + } + return mPlayer.getPlaybackState(); + } + + /** Returns {@code true} when the player is prepared to play, {@code false} otherwise. */ + public boolean isPrepared() { + int state = getPlaybackState(); + return state == ExoPlayer.STATE_READY || state == ExoPlayer.STATE_BUFFERING; + } + + /** Returns {@code true} when the player is being ready to play, {@code false} otherwise. */ + public boolean isPlaying() { + int state = getPlaybackState(); + return (state == ExoPlayer.STATE_READY || state == ExoPlayer.STATE_BUFFERING) + && mPlayer.getPlayWhenReady(); + } + + /** Returns {@code true} when the player is buffering, {@code false} otherwise. */ + public boolean isBuffering() { + return getPlaybackState() == ExoPlayer.STATE_BUFFERING; + } + + /** Returns the current position of the playback in milli seconds. */ + public long getCurrentPosition() { + return mPlayer.getCurrentPosition(); + } + + /** Returns the total duration of the playback. */ + public long getDuration() { + return mPlayer.getDuration(); + } + + /** + * Returns {@code true} when the player is being ready to play, {@code false} when the player is + * paused. + */ + public boolean getPlayWhenReady() { + return mPlayer.getPlayWhenReady(); + } + + /** + * Sets the volume of the audio. + * + * @param volume see also {@link AudioTrack#setVolume(float)} + */ + public void setVolume(float volume) { + mVolume = volume; + if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { + mPlayer.sendMessage( + mAudioRenderer, MpegTsDefaultAudioTrackRenderer.MSG_SET_VOLUME, volume); + } else { + mPlayer.sendMessage( + mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, volume); + } + } + + /** + * Enables or disables audio and closed caption. + * + * @param enable enables the audio and closed caption when {@code true}, disables otherwise. + */ + public void setAudioTrackAndClosedCaption(boolean enable) { + if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { + mPlayer.sendMessage( + mAudioRenderer, + MpegTsDefaultAudioTrackRenderer.MSG_SET_AUDIO_TRACK, + enable ? 1 : 0); + } else { + mPlayer.sendMessage( + mAudioRenderer, + MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, + enable ? mVolume : 0.0f); + } + mPlayer.sendMessage( + mTextRenderer, Cea708TextTrackRenderer.MSG_ENABLE_CLOSED_CAPTION, enable); + } + + /** Returns {@code true} when AC3 audio can be played, {@code false} otherwise. */ + public boolean isAc3Playable() { + return mAudioCapabilities != null + && mAudioCapabilities.supportsEncoding(AudioFormat.ENCODING_AC3); + } + + /** Notifies when the audio cannot be played by the current device. */ + public void onAudioUnplayable() { + if (mListener != null) { + mListener.onAudioUnplayable(); + } + } + + /** Returns {@code true} if the player has any video track, {@code false} otherwise. */ + public boolean hasVideo() { + return mPlayer.getTrackCount(TRACK_TYPE_VIDEO) > 0; + } + + /** Returns {@code true} if the player has any audio trock, {@code false} otherwise. */ + public boolean hasAudio() { + return mPlayer.getTrackCount(TRACK_TYPE_AUDIO) > 0; + } + + /** Returns the number of tracks exposed by the specified renderer. */ + public int getTrackCount(int rendererIndex) { + return mPlayer.getTrackCount(rendererIndex); + } + + /** Selects a track for the specified renderer. */ + public void setSelectedTrack(int rendererIndex, int trackIndex) { + if (trackIndex >= getTrackCount(rendererIndex)) { + return; + } + mPlayer.setSelectedTrack(rendererIndex, trackIndex); + } + + /** + * Returns the index of the currently selected track for the specified renderer. + * + * @param rendererIndex The index of the renderer. + * @return The selected track. A negative value or a value greater than or equal to the + * renderer's track count indicates that the renderer is disabled. + */ + public int getSelectedTrack(int rendererIndex) { + return mPlayer.getSelectedTrack(rendererIndex); + } + + /** + * Returns the format of a track. + * + * @param rendererIndex The index of the renderer. + * @param trackIndex The index of the track. + * @return The format of the track. + */ + public MediaFormat getTrackFormat(int rendererIndex, int trackIndex) { + return mPlayer.getTrackFormat(rendererIndex, trackIndex); + } + + /** Gets the main handler of the player. */ + /* package */ Handler getMainHandler() { + return mMainHandler; + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int state) { + if (mListener == null) { + return; + } + mListener.onStateChanged(playWhenReady, state); + if (state == ExoPlayer.STATE_READY + && mPlayer.getTrackCount(TRACK_TYPE_VIDEO) > 0 + && playWhenReady) { + MediaFormat format = mPlayer.getTrackFormat(TRACK_TYPE_VIDEO, 0); + mListener.onVideoSizeChanged(format.width, format.height, format.pixelWidthHeightRatio); + } + } + + @Override + public void onPlayerError(ExoPlaybackException exception) { + mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + if (mListener != null) { + mListener.onError(exception); + } + } + + @Override + public void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + if (mListener != null) { + mListener.onVideoSizeChanged(width, height, pixelWidthHeightRatio); + } + } + + @Override + public void onDecoderInitialized( + String decoderName, long elapsedRealtimeMs, long initializationDurationMs) { + // Do nothing. + } + + @Override + public void onDecoderInitializationError(DecoderInitializationException e) { + // Do nothing. + } + + @Override + public void onAudioTrackInitializationError(AudioTrack.InitializationException e) { + if (mListener != null) { + mListener.onAudioUnplayable(); + } + } + + @Override + public void onAudioTrackWriteError(AudioTrack.WriteException e) { + // Do nothing. + } + + @Override + public void onAudioTrackUnderrun( + int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + // Do nothing. + } + + @Override + public void onCryptoError(CryptoException e) { + // Do nothing. + } + + @Override + public void onPlayWhenReadyCommitted() { + // Do nothing. + } + + @Override + public void onDrawnToSurface(Surface surface) { + if (mListener != null) { + mListener.onDrawnToSurface(this, surface); + } + } + + @Override + public void onDroppedFrames(int count, long elapsed) { + TunerDebug.notifyVideoFrameDrop(count, elapsed); + if (mTrickplayRunning && mListener != null) { + mListener.onSmoothTrickplayForceStopped(); + } + } + + @Override + public void onAudioTrackSetPlaybackParamsError(IllegalArgumentException e) { + if (mTrickplayRunning && mListener != null) { + mListener.onSmoothTrickplayForceStopped(); + } + } + + private void pushSurface(boolean blockForSurfacePush) { + if (mRendererBuildingState != RENDERER_BUILDING_STATE_BUILT) { + return; + } + + if (blockForSurfacePush) { + mPlayer.blockingSendMessage( + mVideoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, mSurface); + } else { + mPlayer.sendMessage( + mVideoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, mSurface); + } + } + + private void pushTrackSelection(@TrackType int type, boolean allowRendererEnable) { + if (mRendererBuildingState != RENDERER_BUILDING_STATE_BUILT) { + return; + } + mPlayer.setSelectedTrack(type, allowRendererEnable ? 0 : -1); + } + + private class MpegTsCcListener implements Cea708TextTrackRenderer.CcListener { + + @Override + public void emitEvent(CaptionEvent captionEvent) { + if (mVideoEventListener != null) { + mVideoEventListener.onEmitCaptionEvent(captionEvent); + } + } + + @Override + public void clearCaption() { + if (mVideoEventListener != null) { + mVideoEventListener.onClearCaptionEvent(); + } + } + + @Override + public void discoverServiceNumber(int serviceNumber) { + if (mVideoEventListener != null) { + mVideoEventListener.onDiscoverCaptionServiceNumber(serviceNumber); + } + } + } + + private class InternalRendererBuilderCallback implements RendererBuilderCallback { + private boolean canceled; + + public void cancel() { + canceled = true; + } + + @Override + public void onRenderers(String[][] trackNames, TrackRenderer[] renderers) { + if (!canceled) { + MpegTsPlayer.this.onRenderers(renderers); + } + } + + @Override + public void onRenderersError(Exception e) { + if (!canceled) { + MpegTsPlayer.this.onRenderersError(e); + } + } + } +} |