Home | History | Annotate | Download | only in player
      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 
    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
    179     public void onText(String text) {
    180         for (Callback callback : mCallbacks) {
    181             callback.onText(text);
    182         }
    183     }
    184 
    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 
    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 
    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 
    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 
    393     public void setPlayWhenReady(boolean playWhenReady) {
    394         mPlayer.setPlayWhenReady(playWhenReady);
    395     }
    396 
    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 
    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 
    413     public void seekTo(long position) {
    414         mPlayer.seekTo(position);
    415     }
    416 
    417     public void stop() {
    418         mPlayer.stop();
    419     }
    420 
    421     public void release() {
    422         mPlayer.release();
    423     }
    424 
    425     public void addCallback(Callback callback) {
    426         mCallbacks.add(callback);
    427     }
    428 
    429     public void removeCallback(Callback callback) {
    430         mCallbacks.remove(callback);
    431     }
    432 
    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 
    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 {
    460         void onPrepared();
    461         void onPlayerStateChanged(boolean playWhenReady, int state);
    462         void onPlayWhenReadyCommitted();
    463         void onPlayerError(ExoPlaybackException e);
    464         void onDrawnToSurface(Surface surface);
    465         void onText(String text);
    466     }
    467 }
    468