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