1 /*
2  * Copyright 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 package com.example.android.sampletvinput.player;
17 
18 import android.content.Context;
19 import android.content.pm.PackageInfo;
20 import android.content.pm.PackageManager;
21 import android.media.MediaCodec;
22 import android.media.tv.TvTrackInfo;
23 import android.net.Uri;
24 import android.os.Build;
25 import android.os.Handler;
26 import android.view.Surface;
27 
28 import com.google.android.exoplayer.DefaultLoadControl;
29 import com.google.android.exoplayer.DummyTrackRenderer;
30 import com.google.android.exoplayer.ExoPlaybackException;
31 import com.google.android.exoplayer.ExoPlayer;
32 import com.google.android.exoplayer.ExoPlayerLibraryInfo;
33 import com.google.android.exoplayer.FrameworkSampleSource;
34 import com.google.android.exoplayer.LoadControl;
35 import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
36 import com.google.android.exoplayer.MediaCodecTrackRenderer;
37 import com.google.android.exoplayer.MediaCodecUtil;
38 import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
39 import com.google.android.exoplayer.SampleSource;
40 import com.google.android.exoplayer.TrackRenderer;
41 import com.google.android.exoplayer.chunk.ChunkSampleSource;
42 import com.google.android.exoplayer.chunk.ChunkSource;
43 import com.google.android.exoplayer.chunk.Format;
44 import com.google.android.exoplayer.chunk.FormatEvaluator;
45 import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
46 import com.google.android.exoplayer.dash.DashChunkSource;
47 import com.google.android.exoplayer.dash.mpd.AdaptationSet;
48 import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
49 import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser;
50 import com.google.android.exoplayer.dash.mpd.Period;
51 import com.google.android.exoplayer.dash.mpd.Representation;
52 import com.google.android.exoplayer.hls.HlsChunkSource;
53 import com.google.android.exoplayer.hls.HlsPlaylist;
54 import com.google.android.exoplayer.hls.HlsPlaylistParser;
55 import com.google.android.exoplayer.hls.HlsSampleSource;
56 import com.google.android.exoplayer.text.TextRenderer;
57 import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer;
58 import com.google.android.exoplayer.upstream.BufferPool;
59 import com.google.android.exoplayer.upstream.DataSource;
60 import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
61 import com.google.android.exoplayer.upstream.UriDataSource;
62 import com.google.android.exoplayer.util.ManifestFetcher;
63 import com.google.android.exoplayer.util.MimeTypes;
64 import com.google.android.exoplayer.util.Util;
65 
66 import java.io.IOException;
67 import java.util.ArrayList;
68 import java.util.List;
69 import java.util.concurrent.CopyOnWriteArrayList;
70 
71 /**
72  * A wrapper around {@link ExoPlayer} that provides a higher level interface. Designed for
73  * integration with {@link android.media.tv.TvInputService}.
74  */
75 public class TvInputPlayer implements TextRenderer {
76     private static final String TAG = "TvInputPlayer";
77 
78     public static final int SOURCE_TYPE_HTTP_PROGRESSIVE = 0;
79     public static final int SOURCE_TYPE_HLS = 1;
80     public static final int SOURCE_TYPE_MPEG_DASH = 2;
81 
82     public static final int STATE_IDLE = ExoPlayer.STATE_IDLE;
83     public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING;
84     public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING;
85     public static final int STATE_READY = ExoPlayer.STATE_READY;
86     public static final int STATE_ENDED = ExoPlayer.STATE_ENDED;
87 
88     private static final int RENDERER_COUNT = 3;
89     private static final int MIN_BUFFER_MS = 1000;
90     private static final int MIN_REBUFFER_MS = 5000;
91 
92     private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
93     private static final int VIDEO_BUFFER_SEGMENTS = 200;
94     private static final int AUDIO_BUFFER_SEGMENTS = 60;
95     private static final int LIVE_EDGE_LATENCY_MS = 30000;
96 
97     private static final int NO_TRACK_SELECTED = -1;
98 
99     private final Handler mHandler;
100     private final ExoPlayer mPlayer;
101     private TrackRenderer mVideoRenderer;
102     private TrackRenderer mAudioRenderer;
103     private TrackRenderer mTextRenderer;
104     private final CopyOnWriteArrayList<Callback> mCallbacks;
105     private float mVolume;
106     private Surface mSurface;
107     private TvTrackInfo[][] mTvTracks = new TvTrackInfo[RENDERER_COUNT][];
108     private int[] mSelectedTvTracks = new int[RENDERER_COUNT];
109     private MultiTrackChunkSource[] mMultiTrackSources = new MultiTrackChunkSource[RENDERER_COUNT];
110 
111     private final MediaCodecVideoTrackRenderer.EventListener mVideoRendererEventListener =
112             new MediaCodecVideoTrackRenderer.EventListener() {
113         @Override
114         public void onDroppedFrames(int count, long elapsed) {
115             // Do nothing.
116         }
117 
118         @Override
119         public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) {
120             // Do nothing.
121         }
122 
123         @Override
124         public void onDrawnToSurface(Surface surface) {
125             for(Callback callback : mCallbacks) {
126                 callback.onDrawnToSurface(surface);
127             }
128         }
129 
130         @Override
131         public void onDecoderInitializationError(
132                 MediaCodecTrackRenderer.DecoderInitializationException e) {
133             for(Callback callback : mCallbacks) {
134                 callback.onPlayerError(new ExoPlaybackException(e));
135             }
136         }
137 
138         @Override
139         public void onCryptoError(MediaCodec.CryptoException e) {
140             for(Callback callback : mCallbacks) {
141                 callback.onPlayerError(new ExoPlaybackException(e));
142             }
143         }
144     };
145 
TvInputPlayer()146     public TvInputPlayer() {
147         mHandler = new Handler();
148         for (int i = 0; i < RENDERER_COUNT; ++i) {
149             mTvTracks[i] = new TvTrackInfo[0];
150             mSelectedTvTracks[i] = NO_TRACK_SELECTED;
151         }
152         mCallbacks = new CopyOnWriteArrayList<>();
153         mPlayer = ExoPlayer.Factory.newInstance(RENDERER_COUNT, MIN_BUFFER_MS, MIN_REBUFFER_MS);
154         mPlayer.addListener(new ExoPlayer.Listener() {
155             @Override
156             public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
157                 for(Callback callback : mCallbacks) {
158                     callback.onPlayerStateChanged(playWhenReady, playbackState);
159                 }
160             }
161 
162             @Override
163             public void onPlayWhenReadyCommitted() {
164                 for(Callback callback : mCallbacks) {
165                     callback.onPlayWhenReadyCommitted();
166                 }
167             }
168 
169             @Override
170             public void onPlayerError(ExoPlaybackException e) {
171                 for(Callback callback : mCallbacks) {
172                     callback.onPlayerError(e);
173                 }
174             }
175         });
176     }
177 
178     @Override
onText(String text)179     public void onText(String text) {
180         for (Callback callback : mCallbacks) {
181             callback.onText(text);
182         }
183     }
184 
prepare(Context context, final Uri uri, int sourceType)185     public void prepare(Context context, final Uri uri, int sourceType) {
186         if (sourceType == SOURCE_TYPE_HTTP_PROGRESSIVE) {
187             FrameworkSampleSource sampleSource = new FrameworkSampleSource(context, uri, null, 2);
188             mAudioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
189             mVideoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
190                     MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mHandler,
191                     mVideoRendererEventListener, 50);
192             mTextRenderer = new DummyTrackRenderer();
193             prepareInternal();
194         } else if (sourceType == SOURCE_TYPE_HLS) {
195             final String userAgent = getUserAgent(context);
196             HlsPlaylistParser parser = new HlsPlaylistParser();
197             ManifestFetcher<HlsPlaylist> playlistFetcher =
198                     new ManifestFetcher<>(parser, uri.toString(), uri.toString(), userAgent);
199             playlistFetcher.singleLoad(mHandler.getLooper(),
200                     new ManifestFetcher.ManifestCallback<HlsPlaylist>() {
201                         @Override
202                         public void onManifest(String contentId, HlsPlaylist manifest) {
203                             DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
204                             DataSource dataSource = new UriDataSource(userAgent, bandwidthMeter);
205                             HlsChunkSource chunkSource = new HlsChunkSource(dataSource,
206                                     uri.toString(), manifest, bandwidthMeter, null,
207                                     HlsChunkSource.ADAPTIVE_MODE_SPLICE);
208                             HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true,
209                                     2);
210                             mAudioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
211                             mVideoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
212                                     MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mHandler,
213                                     mVideoRendererEventListener, 50);
214                             mTextRenderer = new Eia608TrackRenderer(sampleSource,
215                                     TvInputPlayer.this, mHandler.getLooper());
216                             // TODO: Implement custom HLS source to get the internal track metadata.
217                             mTvTracks[TvTrackInfo.TYPE_SUBTITLE] = new TvTrackInfo[1];
218                             mTvTracks[TvTrackInfo.TYPE_SUBTITLE][0] =
219                                     new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE, "1")
220                                         .build();
221                             prepareInternal();
222                         }
223 
224                         @Override
225                         public void onManifestError(String contentId, IOException e) {
226                             for (Callback callback : mCallbacks) {
227                                 callback.onPlayerError(new ExoPlaybackException(e));
228                             }
229                         }
230                     });
231         } else if (sourceType == SOURCE_TYPE_MPEG_DASH) {
232             final String userAgent = getUserAgent(context);
233             MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
234             final ManifestFetcher<MediaPresentationDescription> manifestFetcher =
235                     new ManifestFetcher<>(parser, uri.toString(), uri.toString(), userAgent);
236             manifestFetcher.singleLoad(mHandler.getLooper(),
237                     new ManifestFetcher.ManifestCallback<MediaPresentationDescription>() {
238                 @Override
239                 public void onManifest(String contentId, MediaPresentationDescription manifest) {
240                     Period period = manifest.periods.get(0);
241                     LoadControl loadControl = new DefaultLoadControl(new BufferPool(
242                             BUFFER_SEGMENT_SIZE));
243                     DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
244 
245                     // Determine which video representations we should use for playback.
246                     int maxDecodableFrameSize;
247                     try {
248                         maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize();
249                     } catch (MediaCodecUtil.DecoderQueryException e) {
250                         for (Callback callback : mCallbacks) {
251                             callback.onPlayerError(new ExoPlaybackException(e));
252                         }
253                         return;
254                     }
255 
256                     int videoAdaptationSetIndex = period.getAdaptationSetIndex(
257                             AdaptationSet.TYPE_VIDEO);
258                     List<Representation> videoRepresentations =
259                             period.adaptationSets.get(videoAdaptationSetIndex).representations;
260                     ArrayList<Integer> videoRepresentationIndexList = new ArrayList<Integer>();
261                     for (int i = 0; i < videoRepresentations.size(); i++) {
262                         Format format = videoRepresentations.get(i).format;
263                         if (format.width * format.height > maxDecodableFrameSize) {
264                             // Filtering stream that device cannot play
265                         } else if (!format.mimeType.equals(MimeTypes.VIDEO_MP4)
266                                 && !format.mimeType.equals(MimeTypes.VIDEO_WEBM)) {
267                             // Filtering unsupported mime type
268                         } else {
269                             videoRepresentationIndexList.add(i);
270                         }
271                     }
272 
273                     // Build the video renderer.
274                     if (videoRepresentationIndexList.isEmpty()) {
275                         mVideoRenderer = new DummyTrackRenderer();
276                     } else {
277                         int[] videoRepresentationIndices = Util.toArray(
278                                 videoRepresentationIndexList);
279                         DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
280                         ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher,
281                                 videoAdaptationSetIndex, videoRepresentationIndices,
282                                 videoDataSource,
283                                 new FormatEvaluator.AdaptiveEvaluator(bandwidthMeter),
284                                 LIVE_EDGE_LATENCY_MS);
285                         ChunkSampleSource videoSampleSource = new ChunkSampleSource(
286                                 videoChunkSource, loadControl,
287                                 VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
288                         mVideoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
289                                 MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mHandler,
290                                 mVideoRendererEventListener, 50);
291                     }
292 
293                     // Build the audio chunk sources.
294                     int audioAdaptationSetIndex = period.getAdaptationSetIndex(
295                             AdaptationSet.TYPE_AUDIO);
296                     AdaptationSet audioAdaptationSet = period.adaptationSets.get(
297                             audioAdaptationSetIndex);
298                     List<ChunkSource> audioChunkSourceList = new ArrayList<ChunkSource>();
299                     List<TvTrackInfo> audioTrackList = new ArrayList<>();
300                     if (audioAdaptationSet != null) {
301                         DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter);
302                         FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();
303                         List<Representation> audioRepresentations =
304                                 audioAdaptationSet.representations;
305                         for (int i = 0; i < audioRepresentations.size(); i++) {
306                             Format format = audioRepresentations.get(i).format;
307                             audioTrackList.add(new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO,
308                                     Integer.toString(i))
309                                     .setAudioChannelCount(format.numChannels)
310                                     .setAudioSampleRate(format.audioSamplingRate)
311                                     .setLanguage(format.language)
312                                     .build());
313                             audioChunkSourceList.add(new DashChunkSource(manifestFetcher,
314                                     audioAdaptationSetIndex, new int[] {i}, audioDataSource,
315                                     audioEvaluator, LIVE_EDGE_LATENCY_MS));
316                         }
317                     }
318 
319                     // Build the audio renderer.
320                     final MultiTrackChunkSource audioChunkSource;
321                     if (audioChunkSourceList.isEmpty()) {
322                         mAudioRenderer = new DummyTrackRenderer();
323                     } else {
324                         audioChunkSource = new MultiTrackChunkSource(audioChunkSourceList);
325                         SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource,
326                                 loadControl, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
327                         mAudioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource);
328                         TvTrackInfo[] tracks = new TvTrackInfo[audioTrackList.size()];
329                         audioTrackList.toArray(tracks);
330                         mTvTracks[TvTrackInfo.TYPE_AUDIO] = tracks;
331                         mSelectedTvTracks[TvTrackInfo.TYPE_AUDIO] = 0;
332                         mMultiTrackSources[TvTrackInfo.TYPE_AUDIO] = audioChunkSource;
333                     }
334 
335                     // Build the text renderer.
336                     mTextRenderer = new DummyTrackRenderer();
337 
338                     prepareInternal();
339                 }
340 
341                 @Override
342                 public void onManifestError(String contentId, IOException e) {
343                     for (Callback callback : mCallbacks) {
344                         callback.onPlayerError(new ExoPlaybackException(e));
345                     }
346                 }
347             });
348         } else {
349             throw new IllegalArgumentException("Unknown source type: " + sourceType);
350         }
351     }
352 
getTracks(int trackType)353     public TvTrackInfo[] getTracks(int trackType) {
354         if (trackType < 0 || trackType >= mTvTracks.length) {
355             throw new IllegalArgumentException("Illegal track type: " + trackType);
356         }
357         return mTvTracks[trackType];
358     }
359 
getSelectedTrack(int trackType)360     public String getSelectedTrack(int trackType) {
361         if (trackType < 0 || trackType >= mTvTracks.length) {
362             throw new IllegalArgumentException("Illegal track type: " + trackType);
363         }
364         if (mSelectedTvTracks[trackType] == NO_TRACK_SELECTED) {
365             return null;
366         }
367         return mTvTracks[trackType][mSelectedTvTracks[trackType]].getId();
368     }
369 
selectTrack(int trackType, String trackId)370     public boolean selectTrack(int trackType, String trackId) {
371         if (trackType < 0 || trackType >= mTvTracks.length) {
372             return false;
373         }
374         if (trackId == null) {
375             mPlayer.setRendererEnabled(trackType, false);
376         } else {
377             int trackIndex = Integer.parseInt(trackId);
378             if (mMultiTrackSources[trackType] == null) {
379                 mPlayer.setRendererEnabled(trackType, true);
380             } else {
381                 boolean playWhenReady = mPlayer.getPlayWhenReady();
382                 mPlayer.setPlayWhenReady(false);
383                 mPlayer.setRendererEnabled(trackType, false);
384                 mPlayer.sendMessage(mMultiTrackSources[trackType],
385                         MultiTrackChunkSource.MSG_SELECT_TRACK, trackIndex);
386                 mPlayer.setRendererEnabled(trackType, true);
387                 mPlayer.setPlayWhenReady(playWhenReady);
388             }
389         }
390         return true;
391     }
392 
setPlayWhenReady(boolean playWhenReady)393     public void setPlayWhenReady(boolean playWhenReady) {
394         mPlayer.setPlayWhenReady(playWhenReady);
395     }
396 
setVolume(float volume)397     public void setVolume(float volume) {
398         mVolume = volume;
399         if (mPlayer != null && mAudioRenderer != null) {
400             mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME,
401                     volume);
402         }
403     }
404 
setSurface(Surface surface)405     public void setSurface(Surface surface) {
406         mSurface = surface;
407         if (mPlayer != null && mVideoRenderer != null) {
408             mPlayer.sendMessage(mVideoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE,
409                     surface);
410         }
411     }
412 
seekTo(long position)413     public void seekTo(long position) {
414         mPlayer.seekTo(position);
415     }
416 
stop()417     public void stop() {
418         mPlayer.stop();
419     }
420 
release()421     public void release() {
422         mPlayer.release();
423     }
424 
addCallback(Callback callback)425     public void addCallback(Callback callback) {
426         mCallbacks.add(callback);
427     }
428 
removeCallback(Callback callback)429     public void removeCallback(Callback callback) {
430         mCallbacks.remove(callback);
431     }
432 
prepareInternal()433     private void prepareInternal() {
434         mPlayer.prepare(mAudioRenderer, mVideoRenderer, mTextRenderer);
435         mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME,
436                 mVolume);
437         mPlayer.sendMessage(mVideoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE,
438                 mSurface);
439         // Disable text track by default.
440         mPlayer.setRendererEnabled(TvTrackInfo.TYPE_SUBTITLE, false);
441         for (Callback callback : mCallbacks) {
442             callback.onPrepared();
443         }
444     }
445 
getUserAgent(Context context)446     public static String getUserAgent(Context context) {
447         String versionName;
448         try {
449             String packageName = context.getPackageName();
450             PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
451             versionName = info.versionName;
452         } catch (PackageManager.NameNotFoundException e) {
453             versionName = "?";
454         }
455         return "SampleTvInput/" + versionName + " (Linux;Android " + Build.VERSION.RELEASE +
456                 ") " + "ExoPlayerLib/" + ExoPlayerLibraryInfo.VERSION;
457     }
458 
459     public interface Callback {
onPrepared()460         void onPrepared();
onPlayerStateChanged(boolean playWhenReady, int state)461         void onPlayerStateChanged(boolean playWhenReady, int state);
onPlayWhenReadyCommitted()462         void onPlayWhenReadyCommitted();
onPlayerError(ExoPlaybackException e)463         void onPlayerError(ExoPlaybackException e);
onDrawnToSurface(Surface surface)464         void onDrawnToSurface(Surface surface);
onText(String text)465         void onText(String text);
466     }
467 }
468