/* * Copyright (C) 2019 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.exoplayer2; import android.content.Context; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.view.Surface; import com.android.tv.tuner.data.Cea708Data; import com.android.tv.tuner.data.Cea708Data.CaptionEvent; import com.android.tv.tuner.data.Cea708Parser; import com.android.tv.tuner.source.TsDataSource; import com.android.tv.tuner.tvinput.debug.TunerDebug; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioListener; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.video.VideoListener; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; /** MPEG-2 TS stream player implementation using ExoPlayer2. */ public class MpegTsPlayerV2 implements Player.EventListener, VideoListener, AudioListener, TextOutput, VideoRendererEventListener { /** Interface definition for a callback to be notified of changes in player state. */ public interface Callback { /** * Called when player state changes. * * @param playbackState notifies the updated player state. */ void onStateChanged(@PlayerState int playbackState); /** Called when player has ended with an error. */ void onError(Exception e); /** Called when size of input video to the player changes. */ void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio); /** Called when player rendered its first frame. */ void onRenderedFirstFrame(); /** Called when audio stream is unplayable. */ void onAudioUnplayable(); /** Called when player drops some frames. */ 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 the discovered caption service number. */ void onDiscoverCaptionServiceNumber(int serviceNumber); } 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; @Retention(RetentionPolicy.SOURCE) @IntDef({STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED}) public @interface PlayerState {} public static final int STATE_IDLE = Player.STATE_IDLE; public static final int STATE_BUFFERING = Player.STATE_BUFFERING; public static final int STATE_READY = Player.STATE_READY; public static final int STATE_ENDED = Player.STATE_ENDED; private final SimpleExoPlayer mPlayer; private final DefaultTrackSelector mTrackSelector; private DefaultTrackSelector.Parameters mTrackSelectorParameters; private TrackGroupArray mLastSeenTrackGroupArray; private TrackSelectionArray mLastSeenTrackSelections; private Callback mCallback; private TsDataSource mDataSource; private VideoEventListener mVideoEventListener; private boolean mCaptionsAvailable = false; /** * Creates MPEG2-TS stream player. * * @param context the application context * @param callback callback for playback state changes */ public MpegTsPlayerV2(Context context, Callback callback) { mTrackSelectorParameters = new DefaultTrackSelector.ParametersBuilder() .setSelectUndeterminedTextLanguage(true) .build(); mTrackSelector = new DefaultTrackSelector(); mTrackSelector.setParameters(mTrackSelectorParameters); mLastSeenTrackGroupArray = null; mPlayer = ExoPlayerFactory.newSimpleInstance(context, mTrackSelector); mPlayer.addListener(this); mPlayer.addVideoListener(this); mPlayer.addAudioListener(this); mPlayer.addTextOutput(this); mCallback = callback; } /** * 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) { if (captionServiceNumber == Cea708Data.EMPTY_SERVICE_NUMBER) return; MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo != null) { int rendererCount = mappedTrackInfo.getRendererCount(); for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { if (mappedTrackInfo.getRendererType(rendererIndex) == C.TRACK_TYPE_TEXT) { TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); for (int i = 0; i < trackGroupArray.length; i++) { int readServiceNumber = trackGroupArray.get(i).getFormat(0).accessibilityChannel; int serviceNumber = readServiceNumber == Format.NO_VALUE ? 1 : readServiceNumber; if (serviceNumber == captionServiceNumber) { setSelectedTrack(TRACK_TYPE_TEXT, i); } } } } } } /** * Invoked each time there is a change in the {@link Cue}s to be rendered * * @param cues The {@link Cue}s to be rendered, or an empty list if no cues are to be rendered. */ @Override public void onCues(List cues) { if (!mCaptionsAvailable && cues != null && cues.size() != 0) { mCaptionsAvailable = true; onTracksChanged(mLastSeenTrackGroupArray, mLastSeenTrackSelections); } mVideoEventListener.onEmitCaptionEvent( new CaptionEvent( Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DFX, new Cea708Data.CaptionWindow( /* id= */ 0, /* visible= */ true, /* rowlock= */ false, /* columnLock= */ false, /* priority= */ 3, /* relativePositioning= */ true, /* anchorVertical= */ 0, /* anchorHorizontal= */ 0, /* anchorId= */ 0, /* rowCount= */ 0, /* columnCount= */ 0, /* penStyle= */ 0, /* windowStyle= */ 2))); mVideoEventListener.onEmitCaptionEvent( new CaptionEvent(Cea708Parser.CAPTION_EMIT_TYPE_BUFFER, cues)); } /** * Sets the surface for the player. * * @param surface the {@link Surface} to render video */ public void setSurface(Surface surface) { mPlayer.setVideoSurface(surface); } /** * Prepares player. */ public void prepare(TsDataSource dataSource, MediaSource mediaSource) { mDataSource = dataSource; mPlayer.prepare(mediaSource, false, false); } /** Returns {@link TsDataSource} which provides MPEG2-TS stream. */ public TsDataSource getDataSource() { return mDataSource; } /** * 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); } /** * 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); } /** Releases the player. */ public void release() { if (mDataSource != null) { mDataSource = null; } mCaptionsAvailable = false; mCallback = null; mPlayer.release(); } /** Returns the current status of the player. */ public int getPlaybackState() { 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(); } /** * Sets the volume of the audio. * * @param volume see also * {@link com.google.android.exoplayer2.Player.AudioComponent#setVolume(float)} */ public void setVolume(float volume) { mPlayer.setVolume(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) { //TODO Add handling to enable/disable audio and captions } @Override public void onTimelineChanged( Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {} @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { if (trackGroups != mLastSeenTrackGroupArray) { MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo(); if (mCallback != null && mappedTrackInfo != null && mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { mCallback.onAudioUnplayable(); } mLastSeenTrackGroupArray = trackGroups; } if (trackSelections != mLastSeenTrackSelections) { mLastSeenTrackSelections = trackSelections; } if (mVideoEventListener != null && mCaptionsAvailable) { MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo != null) { int rendererCount = mappedTrackInfo.getRendererCount(); for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { if (mappedTrackInfo.getRendererType(rendererIndex) == C.TRACK_TYPE_TEXT) { TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); for (int i = 0; i < trackGroupArray.length; i++) { int serviceNumber = trackGroupArray.get(i).getFormat(0).accessibilityChannel; mVideoEventListener.onDiscoverCaptionServiceNumber( serviceNumber == Format.NO_VALUE ? 1 : serviceNumber); } } } } } } /** * Checks the stream for the renderer of required track type. * * @param trackType Returns {@code true} if the player has any renderer for track type * {@trackType}, {@code false} otherwise. */ private boolean hasRendererType(int trackType) { MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo != null) { int rendererCount = mappedTrackInfo.getRendererCount(); for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { if (mappedTrackInfo.getRendererType(rendererIndex) == trackType) { return true; } } } return false; } /** Returns {@code true} if the player has any video track, {@code false} otherwise. */ public boolean hasVideo() { return hasRendererType(C.TRACK_TYPE_VIDEO); } /** Returns {@code true} if the player has any audio track, {@code false} otherwise. */ public boolean hasAudio() { return hasRendererType(C.TRACK_TYPE_AUDIO); } /** Returns the number of tracks exposed by the specified renderer. */ public int getTrackCount(int rendererIndex) { MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo != null) { TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); return trackGroupArray.length; } return 0; } /** Selects a track for the specified renderer. */ public void setSelectedTrack(int rendererIndex, int trackIndex) { MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo(); if (trackIndex >= getTrackCount(rendererIndex)) { return; } TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); mTrackSelectorParameters = mTrackSelector.getParameters(); DefaultTrackSelector.SelectionOverride override = new DefaultTrackSelector.SelectionOverride(trackIndex, 0); DefaultTrackSelector.ParametersBuilder builder = mTrackSelectorParameters.buildUpon() .clearSelectionOverrides(rendererIndex) .setRendererDisabled(rendererIndex, false) .setSelectionOverride(rendererIndex, trackGroupArray, override); mTrackSelector.setParameters(builder); } /** * 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) { TrackSelection trackSelection = mPlayer.getCurrentTrackSelections().get(rendererIndex); MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo(); TrackGroupArray trackGroupArray; if (trackSelection != null && mappedTrackInfo != null) { trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); return trackGroupArray.indexOf(trackSelection.getTrackGroup()); } return C.INDEX_UNSET; } /** * 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 Format getTrackFormat(int rendererIndex, int trackIndex) { MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo != null) { TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); TrackGroup trackGroup = trackGroupArray.get(trackIndex); return trackGroup.getFormat(0); } return null; } @Override public void onPlayerStateChanged(boolean playWhenReady, @PlayerState int state) { if (mCallback == null) { return; } mCallback.onStateChanged(state); if (state == STATE_READY && hasVideo() && playWhenReady) { Format format = mPlayer.getVideoFormat(); mCallback.onVideoSizeChanged(format.width, format.height, format.pixelWidthHeightRatio); } } @Override public void onPlayerError(ExoPlaybackException exception) { if (mCallback != null) { mCallback.onError(exception); } } @Override public void onVideoSizeChanged( int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { if (mCallback != null) { mCallback.onVideoSizeChanged(width, height, pixelWidthHeightRatio); } } @Override public void onSurfaceSizeChanged(int width, int height) {} @Override public void onRenderedFirstFrame() { if (mCallback != null) { mCallback.onRenderedFirstFrame(); } } @Override public void onDroppedFrames(int count, long elapsed) { TunerDebug.notifyVideoFrameDrop(count, elapsed); if (mCallback != null) { mCallback.onSmoothTrickplayForceStopped(); } } }