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 " 456 + Build.VERSION.RELEASE_OR_CODENAME + 457 ") " + "ExoPlayerLib/" + ExoPlayerLibraryInfo.VERSION; 458 } 459 460 public interface Callback { onPrepared()461 void onPrepared(); onPlayerStateChanged(boolean playWhenReady, int state)462 void onPlayerStateChanged(boolean playWhenReady, int state); onPlayWhenReadyCommitted()463 void onPlayWhenReadyCommitted(); onPlayerError(ExoPlaybackException e)464 void onPlayerError(ExoPlaybackException e); onDrawnToSurface(Surface surface)465 void onDrawnToSurface(Surface surface); onText(String text)466 void onText(String text); 467 } 468 } 469