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