diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 0000000000..056b47a1e8 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,495 @@ + + + + + + diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fd20664692..3c05471a89 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,197 @@ ### dev-v2 (not yet released) ### +* Add a flag to opt-in to automatic audio focus handling via + `SimpleExoPlayer.setAudioAttributes`. +* Distribute Cronet extension via jCenter. +* Set compileSdkVersion and targetSdkVersion to 28. +* Add `AudioListener` for listening to changes in audio configuration during + playback ([#3994](https://github.com/google/ExoPlayer/issues/3994)). +* Improved seeking support: + * Support seeking in MPEG-TS + ([#966](https://github.com/google/ExoPlayer/issues/966)). + * Support seeking in MPEG-PS + ([#4476](https://github.com/google/ExoPlayer/issues/4476)). + * Support approximate seeking in ADTS using a constant bitrate assumption + ([#4548](https://github.com/google/ExoPlayer/issues/4548)). Note that the + `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` flag must be set on the extractor to + enable this functionality. + * Support approximate seeking in AMR using a constant bitrate assumption. + Note that the `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` flag must be set on the + extractor to enable this functionality. + * Add `DefaultExtractorsFactory.setConstantBitrateSeekingEnabled` to enable + approximate seeking using a constant bitrate assumption for all extractors + that support it. +* MPEG-TS: Support CEA-608/708 in H262 + ([#2565](https://github.com/google/ExoPlayer/issues/2565)). +* MediaSession extension: Allow apps to set custom errors. +* Audio: + * Add support for mu-law and A-law PCM with the ffmpeg extension + ([#4360](https://github.com/google/ExoPlayer/issues/4360)). + * Increase `AudioTrack` buffer sizes to the theoretical maximum required for + each encoding for passthrough playbacks + ([#3803](https://github.com/google/ExoPlayer/issues/3803)). + * Add support for attaching auxiliary audio effects to the `AudioTrack`. + * Add support for seamless adaptation while playing xHE-AAC streams. +* Video: + * Add callback to `VideoListener` to notify of surface size changes. + * Scale up the initial video decoder maximum input size so playlist item + transitions with small increases in maximum sample size don't require + reinitialization ([#4510](https://github.com/google/ExoPlayer/issues/4510)). +* Allow apps to pass a `CacheKeyFactory` for setting custom cache keys when + creating a `CacheDataSource`. +* Turned on Java 8 compiler support for the ExoPlayer library. Apps that depend + on ExoPlayer via its source code rather than an AAR may need to add + `compileOptions { targetCompatibility JavaVersion.VERSION_1_8 }` to their + gradle settings to ensure bytecode compatibility. +* ConcatenatingMediaSource: + * Add support for lazy preparation of playlist media sources + ([#3972](https://github.com/google/ExoPlayer/issues/3972)). + * Add support for range removal with `removeMediaSourceRange` methods. +* `BandwidthMeter` management: + * Pass `BandwidthMeter` directly to `ExoPlayerFactory` instead of + `TrackSelection.Factory` and `DataSource.Factory`. May also be omitted to + use the default bandwidth meter automatically. This change only works + correctly if the following changes are adopted for custom `BandwidthMeter`s, + `TrackSelection`s, `MediaSource`s and `DataSource`s. + * Pass `BandwidthMeter` to `TrackSelection.Factory` which should be used to + obtain bandwidth estimates. + * Add method to `BandwidthMeter` to return the `TransferListener` used to + gather bandwidth information. Also add methods to add and remove event + listeners. + * Pass `TransferListener` to `MediaSource`s to listen to media data transfers. + * Add method to `DataSource` to add `TransferListener`s. Custom `DataSource`s + directly reading data should implement `BaseDataSource` to handle the + registration correctly. Custom `DataSource`'s forwarding to other sources + should forward all calls to `addTransferListener`. + * Extend `TransferListener` with additional callback parameters. +* Error handling: + * Allow configuration of the Loader retry delay + ([#3370](https://github.com/google/ExoPlayer/issues/3370)). +* HLS: + * Add support for PlayReady. + * Add support for alternative EXT-X-KEY tags. + * Set the bitrate on primary track sample formats + ([#3297](https://github.com/google/ExoPlayer/issues/3297)). + * Pass HTTP response headers to `HlsExtractorFactory.createExtractor`. + * Add support for EXT-X-INDEPENDENT-SEGMENTS in the master playlist. + * Support load error handling customization + ([#2981](https://github.com/google/ExoPlayer/issues/2981)). +* Fix bug when reporting buffered position for multi-period windows and add + two additional convenience methods `Player.getTotalBufferedDuration` and + `Player.getContentBufferedDuration` + ([#4023](https://github.com/google/ExoPlayer/issues/4023)). +* MediaSession extension: + * Allow apps to set custom metadata with a MediaMetadataProvider + ([#3497](https://github.com/google/ExoPlayer/issues/3497)). +* Improved performance when playing high frame-rate content, and when playing + at greater than 1x speed + ([#2777](https://github.com/google/ExoPlayer/issues/2777)). +* Allow setting the `Looper`, which is used to access the player, in + `ExoPlayerFactory` ([#4278](https://github.com/google/ExoPlayer/issues/4278)). +* Use default Deserializers if non given to DownloadManager. +* Add monoscopic 360 surface type to PlayerView. +* Deprecate `Player.DefaultEventListener` as selective listener overrides can + be directly made with the `Player.EventListener` interface. +* Deprecate `DefaultAnalyticsListener` as selective listener overrides can be + directly made with the `AnalyticsListener` interface. +* Add uri field to `LoadEventInfo` in `MediaSourceEventListener` or + `AnalyticsListener` callbacks. This uri is the redirected uri if redirection + occurred ([#2054](https://github.com/google/ExoPlayer/issues/2054)). +* Allow `MediaCodecSelector`s to return multiple compatible decoders for + `MediaCodecRenderer`, and provide an (optional) `MediaCodecSelector` that + falls back to less preferred decoders like `MediaCodec.createDecoderByType` + ([#273](https://github.com/google/ExoPlayer/issues/273)). +* Fix where transitions to clipped media sources happened too early + ([#4583](https://github.com/google/ExoPlayer/issues/4583)). +* Add `DataSpec.httpMethod` and update `HttpDataSource` implementations to + support HTTP HEAD method. Previously, only GET and POST were supported. +* Add option to show buffering view when playWhenReady is false + ([#4304](https://github.com/google/ExoPlayer/issues/4304)). +* Allow any `Drawable` to be used as `PlayerView` default artwork. + +### 2.8.4 ### + +* IMA: Improve handling of consecutive empty ad groups + ([#4030](https://github.com/google/ExoPlayer/issues/4030)), + ([#4280](https://github.com/google/ExoPlayer/issues/4280)). + +### 2.8.3 ### + +* IMA: + * Fix behavior when creating/releasing the player then releasing + `ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)). + * Add support for setting slots for companion ads. +* Captions: + * TTML: Fix an issue with TTML using font size as % of cell resolution that + makes `SubtitleView.setApplyEmbeddedFontSizes()` not work correctly. + ([#4491](https://github.com/google/ExoPlayer/issues/4491)). + * CEA-608: Improve handling of embedded styles + ([#4321](https://github.com/google/ExoPlayer/issues/4321)). +* DASH: + * Exclude text streams from duration calculations + ([#4029](https://github.com/google/ExoPlayer/issues/4029)). + * Fix freezing when playing multi-period manifests with `EventStream`s + ([#4492](https://github.com/google/ExoPlayer/issues/4492)). +* DRM: Allow DrmInitData to carry a license server URL + ([#3393](https://github.com/google/ExoPlayer/issues/3393)). +* MPEG-TS: Fix bug preventing SCTE-35 cues from being output + ([#4573](https://github.com/google/ExoPlayer/issues/4573)). +* Expose all internal ID3 data stored in MP4 udta boxes, and switch from using + CommentFrame to InternalFrame for frames with gapless metadata in MP4. +* Add `PlayerView.isControllerVisible` + ([#4385](https://github.com/google/ExoPlayer/issues/4385)). +* Fix issue playing DRM protected streams on Asus Zenfone 2 + ([#4403](https://github.com/google/ExoPlayer/issues/4413)). +* Add support for multiple audio and video tracks in MPEG-PS streams + ([#4406](https://github.com/google/ExoPlayer/issues/4406)). +* Add workaround for track index mismatches between trex and tkhd boxes in + fragmented MP4 files + ([#4477](https://github.com/google/ExoPlayer/issues/4477)). +* Add workaround for track index mismatches between tfhd and tkhd boxes in + fragmented MP4 files + ([#4083](https://github.com/google/ExoPlayer/issues/4083)). +* Ignore all MP4 edit lists if one edit list couldn't be handled + ([#4348](https://github.com/google/ExoPlayer/issues/4348)). +* Fix issue when switching track selection from an embedded track to a primary + track in DASH ([#4477](https://github.com/google/ExoPlayer/issues/4477)). +* Fix accessibility class name for `DefaultTimeBar` + ([#4611](https://github.com/google/ExoPlayer/issues/4611)). +* Improved compatibility with FireOS devices. + +### 2.8.2 ### + +* IMA: Don't advertise support for video/mpeg ad media, as we don't have an + extractor for this ([#4297](https://github.com/google/ExoPlayer/issues/4297)). +* DASH: Fix playback getting stuck when playing representations that have both + sidx atoms and non-zero presentationTimeOffset values. +* HLS: + * Allow injection of custom playlist trackers. + * Fix adaptation in live playlists with EXT-X-PROGRAM-DATE-TIME tags. +* Mitigate memory leaks when `MediaSource` loads are slow to cancel + ([#4249](https://github.com/google/ExoPlayer/issues/4249)). +* Fix inconsistent `Player.EventListener` invocations for recursive player state + changes ([#4276](https://github.com/google/ExoPlayer/issues/4276)). +* Fix `MediaCodec.native_setSurface` crash on Moto C + ([#4315](https://github.com/google/ExoPlayer/issues/4315)). +* Fix missing whitespace in CEA-608 + ([#3906](https://github.com/google/ExoPlayer/issues/3906)). +* Fix crash downloading HLS media playlists + ([#4396](https://github.com/google/ExoPlayer/issues/4396)). +* Fix a bug where download cancellation was ignored + ([#4403](https://github.com/google/ExoPlayer/issues/4403)). +* Set `METADATA_KEY_TITLE` on media descriptions + ([#4292](https://github.com/google/ExoPlayer/issues/4292)). +* Allow apps to register custom MIME types + ([#4264](https://github.com/google/ExoPlayer/issues/4264)). + +### 2.8.1 ### + +* HLS: + * Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags + ([#4239](https://github.com/google/ExoPlayer/issues/4239)). + * Fix playback of clipped streams starting from non-keyframe positions + ([#4241](https://github.com/google/ExoPlayer/issues/4241)). * OkHttp extension: Fix to correctly include response headers in thrown `InvalidResponseCodeException`s. * Add possibility to cancel `PlayerMessage`s. @@ -19,10 +210,7 @@ ([#4228](https://github.com/google/ExoPlayer/issues/4228)). * FLAC: Supports seeking for FLAC files without SEEKTABLE ([#1808](https://github.com/google/ExoPlayer/issues/1808)). -* HLS: - * Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags - ([#4239](https://github.com/google/ExoPlayer/issues/4239)). -* Caption: +* Captions: * TTML: * Fix a styling issue when there are multiple regions displayed at the same time that can make text size of each region much smaller than defined. @@ -57,7 +245,7 @@ periods are created, released and being read from. * Support live stream clipping with `ClippingMediaSource`. * Allow setting tags for all media sources in their factories. The tag of the - current window can be retrieved with `ExoPlayer.getCurrentTag`. + current window can be retrieved with `Player.getCurrentTag`. * UI components: * Add support for displaying error messages and a buffering spinner in `PlayerView`. diff --git a/build.gradle b/build.gradle index 3813a241e0..a013f4fb84 100644 --- a/build.gradle +++ b/build.gradle @@ -17,8 +17,9 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.0' + classpath 'com.android.tools.build:gradle:3.1.4' classpath 'com.novoda:bintray-release:0.8.1' + classpath 'com.google.android.gms:strict-version-matcher-plugin:1.0.3' } // Workaround for the following test coverage issue. Remove when fixed: // https://code.google.com/p/android/issues/detail?id=226070 diff --git a/constants.gradle b/constants.gradle index dcadcceb4f..c1776b86c4 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,19 +13,18 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.8.0' - releaseVersionCode = 2800 + releaseVersion = '2.8.4' + releaseVersionCode = 2804 // Important: ExoPlayer specifies a minSdkVersion of 14 because various // components provided by the library may be of use on older devices. // However, please note that the core media playback functionality provided // by the library requires API level 16 or greater. minSdkVersion = 14 - targetSdkVersion = 27 - compileSdkVersion = 27 - buildToolsVersion = '27.0.3' + targetSdkVersion = 28 + compileSdkVersion = 28 + buildToolsVersion = '28.0.2' testSupportLibraryVersion = '0.5' - supportLibraryVersion = '27.0.0' - playServicesLibraryVersion = '12.0.0' + supportLibraryVersion = '27.1.1' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' junitVersion = '4.12' @@ -33,6 +32,7 @@ project.ext { robolectricVersion = '3.7.1' autoValueVersion = '1.6' checkerframeworkVersion = '2.5.0' + testRunnerVersion = '1.1.0-alpha3' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/core_settings.gradle b/core_settings.gradle index fc738c8476..4d90fa962a 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -30,6 +30,7 @@ include modulePrefix + 'extension-flac' include modulePrefix + 'extension-gvr' include modulePrefix + 'extension-ima' include modulePrefix + 'extension-cast' +include modulePrefix + 'extension-cronet' include modulePrefix + 'extension-mediasession' include modulePrefix + 'extension-okhttp' include modulePrefix + 'extension-opus' @@ -51,6 +52,7 @@ project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensi project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima') project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast') +project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet') project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession') project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp') project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus') @@ -58,9 +60,3 @@ project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensio project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp') project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback') project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher') - -if (gradle.ext.has('exoplayerIncludeCronetExtension') - && gradle.ext.exoplayerIncludeCronetExtension) { - include modulePrefix + 'extension-cronet' - project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet') -} diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index ae6bdd1d94..915bc10b7c 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode @@ -57,3 +62,5 @@ dependencies { implementation 'com.android.support:appcompat-v7:' + supportLibraryVersion implementation 'com.android.support:recyclerview-v7:' + supportLibraryVersion } + +apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 63b18b0aa7..d188469de8 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -17,14 +17,15 @@ package com.google.android.exoplayer2.castdemo; import android.content.Context; import android.net.Uri; +import android.support.annotation.Nullable; import android.view.KeyEvent; import android.view.View; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Player.DefaultEventListener; import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.Player.TimelineChangeReason; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; @@ -36,14 +37,11 @@ import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; @@ -51,11 +49,9 @@ import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; import java.util.ArrayList; -/** - * Manages players and an internal media queue for the ExoPlayer/Cast demo app. - */ -/* package */ final class PlayerManager extends DefaultEventListener - implements CastPlayer.SessionAvailabilityListener { +/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */ +/* package */ final class PlayerManager + implements EventListener, CastPlayer.SessionAvailabilityListener { /** * Listener for changes in the media queue playback position. @@ -70,9 +66,8 @@ import java.util.ArrayList; } private static final String USER_AGENT = "ExoCastDemoPlayer"; - private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = - new DefaultHttpDataSourceFactory(USER_AGENT, BANDWIDTH_METER); + new DefaultHttpDataSourceFactory(USER_AGENT); private final PlayerView localPlayerView; private final PlayerControlView castControlView; @@ -119,9 +114,9 @@ import java.util.ArrayList; currentItemIndex = C.INDEX_UNSET; concatenatingMediaSource = new ConcatenatingMediaSource(); - DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER); + DefaultTrackSelector trackSelector = new DefaultTrackSelector(); RenderersFactory renderersFactory = new DefaultRenderersFactory(context); - exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); + exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector); exoPlayer.addListener(this); localPlayerView.setPlayer(exoPlayer); @@ -282,7 +277,7 @@ import java.util.ArrayList; @Override public void onTimelineChanged( - Timeline timeline, Object manifest, @TimelineChangeReason int reason) { + Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { updateCurrentItemIndex(); if (timeline.isEmpty()) { castMediaQueueCreationPending = true; @@ -396,13 +391,9 @@ import java.util.ArrayList; Uri uri = Uri.parse(sample.uri); switch (sample.mimeType) { case DemoUtil.MIME_TYPE_SS: - return new SsMediaSource.Factory( - new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY) - .createMediaSource(uri); + return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); case DemoUtil.MIME_TYPE_DASH: - return new DashMediaSource.Factory( - new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY) - .createMediaSource(uri); + return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); case DemoUtil.MIME_TYPE_HLS: return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); case DemoUtil.MIME_TYPE_VIDEO_MP4: diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index 35c2daf88e..33cca6ef46 100644 --- a/demos/ima/build.gradle +++ b/demos/ima/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode @@ -51,3 +56,5 @@ dependencies { implementation project(modulePrefix + 'extension-ima') implementation 'com.android.support:support-annotations:' + supportLibraryVersion } + +apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index 4fab1966fe..97e618ba52 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -27,18 +27,14 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.util.Util; @@ -46,8 +42,7 @@ import com.google.android.exoplayer2.util.Util; /* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory { private final ImaAdsLoader adsLoader; - private final DataSource.Factory manifestDataSourceFactory; - private final DataSource.Factory mediaDataSourceFactory; + private final DataSource.Factory dataSourceFactory; private SimpleExoPlayer player; private long contentPosition; @@ -55,21 +50,14 @@ import com.google.android.exoplayer2.util.Util; public PlayerManager(Context context) { String adTag = context.getString(R.string.ad_tag_url); adsLoader = new ImaAdsLoader(context, Uri.parse(adTag)); - manifestDataSourceFactory = + dataSourceFactory = new DefaultDataSourceFactory( context, Util.getUserAgent(context, context.getString(R.string.application_name))); - mediaDataSourceFactory = - new DefaultDataSourceFactory( - context, - Util.getUserAgent(context, context.getString(R.string.application_name)), - new DefaultBandwidthMeter()); } public void init(Context context, PlayerView playerView) { // Create a default track selector. - BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); - TrackSelection.Factory videoTrackSelectionFactory = - new AdaptiveTrackSelection.Factory(bandwidthMeter); + TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); // Create a player instance. @@ -133,18 +121,13 @@ import com.google.android.exoplayer2.util.Util; @ContentType int type = Util.inferContentType(uri); switch (type) { case C.TYPE_DASH: - return new DashMediaSource.Factory( - new DefaultDashChunkSource.Factory(mediaDataSourceFactory), - manifestDataSourceFactory) - .createMediaSource(uri); + return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_SS: - return new SsMediaSource.Factory( - new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory) - .createMediaSource(uri); + return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_HLS: - return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); + return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_OTHER: - return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); + return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri); default: throw new IllegalStateException("Unsupported type: " + type); } diff --git a/demos/main/build.gradle b/demos/main/build.gradle index ce0992eb7a..c516ba297f 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode @@ -70,3 +75,5 @@ dependencies { withExtensionsImplementation project(path: modulePrefix + 'extension-vp9') withExtensionsImplementation project(path: modulePrefix + 'extension-rtmp') } + +apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 3bedefc60e..2234048ac1 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ package="com.google.android.exoplayer2.demo"> + @@ -78,7 +79,7 @@ - + diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 0d26f196c1..a366eeba05 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -4,22 +4,22 @@ "samples": [ { "name": "Google Glass (MP4,H264)", - "uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0", + "uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0", "extension": "mpd" }, { "name": "Google Play (MP4,H264)", - "uri": "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0", + "uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0", "extension": "mpd" }, { "name": "Google Glass (WebM,VP9)", - "uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0", + "uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0", "extension": "mpd" }, { "name": "Google Play (WebM,VP9)", - "uri": "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0", + "uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0", "extension": "mpd" } ] @@ -330,11 +330,11 @@ "samples": [ { "name": "Super speed", - "uri": "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism" + "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism" }, { "name": "Super speed (PlayReady)", - "uri": "http://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism", + "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism", "drm_scheme": "playready" } ] @@ -365,10 +365,6 @@ { "name": "Apple AAC media playlist", "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8" - }, - { - "name": "Apple ID3 metadata", - "uri": "http://devimages.apple.com/samplecode/adDemo/ad.m3u8" } ] }, @@ -376,7 +372,7 @@ "name": "Misc", "samples": [ { - "name": "Dizzy", + "name": "Dizzy (MP4)", "uri": "https://html5demos.com/assets/dizzy.mp4" }, { @@ -391,10 +387,6 @@ "name": "Android screens (Matroska)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" }, - { - "name": "Big Buck Bunny (MP4 Video)", - "uri": "http://redirector.c.youtube.com/videoplayback?id=604ed5ce52eda7ee&itag=22&source=youtube&sparams=ip,ipbits,expire,source,id&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=513F28C7FDCBEC60A66C86C9A393556C99DC47FB.04C88036EEE12565A1ED864A875A58F15D8B5300&key=ik0" - }, { "name": "Screens 360P (WebM,VP9,No Audio)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm" @@ -419,21 +411,9 @@ "name": "Google Play (Ogg/Vorbis Audio)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg" }, - { - "name": "Google Glass (WebM Video with Vorbis Audio)", - "uri": "http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm" - }, - { - "name": "Google Glass (VP9 in MP4/ISO-BMFF)", - "uri": "http://demos.webmproject.org/exoplayer/glass.mp4" - }, - { - "name": "Google Glass DASH - VP9 and Opus", - "uri": "http://demos.webmproject.org/dash/201410/vp9_glass/manifest_vp9_opus.mpd" - }, { "name": "Big Buck Bunny (FLV Video)", - "uri": "http://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0" + "uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0" } ] }, @@ -570,23 +550,27 @@ { "name": "VMAP empty midroll", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll" + "ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll" }, { "name": "VMAP full, empty, full midrolls", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll-2" + "ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll-2" } ] }, { - "name": "ABR", + "name": "360", "samples": [ { - "name": "Random ABR - Google Glass (MP4,H264)", - "uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0", - "extension": "mpd", - "abr_algorithm": "random" + "name": "Congo (360 top-bottom stereo)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4", + "spherical_stereo_mode": "top_bottom" + }, + { + "name": "Iceland (360 top-bottom stereo ts)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts", + "spherical_stereo_mode": "top_bottom" } ] } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java index b5c127d2e3..ac8be7dc16 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java @@ -16,19 +16,13 @@ package com.google.android.exoplayer2.demo; import android.app.Application; -import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.offline.ProgressiveDownloadAction; -import com.google.android.exoplayer2.source.dash.offline.DashDownloadAction; -import com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction; -import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.FileDataSourceFactory; import com.google.android.exoplayer2.upstream.HttpDataSource; -import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; @@ -46,13 +40,6 @@ public class DemoApplication extends Application { private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2; - private static final Deserializer[] DOWNLOAD_DESERIALIZERS = - new Deserializer[] { - DashDownloadAction.DESERIALIZER, - HlsDownloadAction.DESERIALIZER, - SsDownloadAction.DESERIALIZER, - ProgressiveDownloadAction.DESERIALIZER - }; protected String userAgent; @@ -68,16 +55,15 @@ public class DemoApplication extends Application { } /** Returns a {@link DataSource.Factory}. */ - public DataSource.Factory buildDataSourceFactory(TransferListener listener) { + public DataSource.Factory buildDataSourceFactory() { DefaultDataSourceFactory upstreamFactory = - new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener)); + new DefaultDataSourceFactory(this, buildHttpDataSourceFactory()); return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache()); } /** Returns a {@link HttpDataSource.Factory}. */ - public HttpDataSource.Factory buildHttpDataSourceFactory( - TransferListener listener) { - return new DefaultHttpDataSourceFactory(userAgent, listener); + public HttpDataSource.Factory buildHttpDataSourceFactory() { + return new DefaultHttpDataSourceFactory(userAgent); } /** Returns whether extension renderers should be used. */ @@ -98,21 +84,18 @@ public class DemoApplication extends Application { private synchronized void initDownloadManager() { if (downloadManager == null) { DownloaderConstructorHelper downloaderConstructorHelper = - new DownloaderConstructorHelper( - getDownloadCache(), buildHttpDataSourceFactory(/* listener= */ null)); + new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); downloadManager = new DownloadManager( downloaderConstructorHelper, MAX_SIMULTANEOUS_DOWNLOADS, DownloadManager.DEFAULT_MIN_RETRY_COUNT, - new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE), - DOWNLOAD_DESERIALIZERS); + new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE)); downloadTracker = new DownloadTracker( /* context= */ this, - buildDataSourceFactory(/* listener= */ null), - new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE), - DOWNLOAD_DESERIALIZERS); + buildDataSourceFactory(), + new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE)); downloadManager.addListener(downloadTracker); } } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java index b4bce01c7a..f20e41d8f7 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -36,7 +36,7 @@ import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager.TaskState; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper; -import com.google.android.exoplayer2.offline.SegmentDownloadAction; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.TrackKey; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -85,7 +85,7 @@ public class DownloadTracker implements DownloadManager.Listener { Context context, DataSource.Factory dataSourceFactory, File actionFile, - DownloadAction.Deserializer[] deserializers) { + DownloadAction.Deserializer... deserializers) { this.context = context.getApplicationContext(); this.dataSourceFactory = dataSourceFactory; this.actionFile = new ActionFile(actionFile); @@ -95,7 +95,8 @@ public class DownloadTracker implements DownloadManager.Listener { HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker"); actionFileWriteThread.start(); actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper()); - loadTrackedActions(deserializers); + loadTrackedActions( + deserializers.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers()); } public void addListener(Listener listener) { @@ -111,15 +112,11 @@ public class DownloadTracker implements DownloadManager.Listener { } @SuppressWarnings("unchecked") - public List getOfflineStreamKeys(Uri uri) { + public List getOfflineStreamKeys(Uri uri) { if (!trackedDownloadStates.containsKey(uri)) { return Collections.emptyList(); } - DownloadAction action = trackedDownloadStates.get(uri); - if (action instanceof SegmentDownloadAction) { - return ((SegmentDownloadAction) action).keys; - } - return Collections.emptyList(); + return trackedDownloadStates.get(uri).getKeys(); } public void toggleDownload(Activity activity, String name, Uri uri, String extension) { @@ -270,11 +267,11 @@ public class DownloadTracker implements DownloadManager.Listener { trackTitles.add(trackNameProvider.getTrackName(trackGroup.getFormat(k))); } } - if (!trackKeys.isEmpty()) { - builder.setView(dialogView); - } - builder.create().show(); } + if (!trackKeys.isEmpty()) { + builder.setView(dialogView); + } + builder.create().show(); } @Override @@ -282,6 +279,7 @@ public class DownloadTracker implements DownloadManager.Listener { Toast.makeText( context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG) .show(); + Log.e(TAG, "Failed to start download", e); } @Override diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 091e483155..ad08fb990c 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -49,6 +49,7 @@ import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.offline.FilteringManifestParser; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; @@ -57,16 +58,11 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; -import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; -import com.google.android.exoplayer2.source.hls.playlist.RenditionKey; -import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.StreamKey; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; @@ -77,8 +73,8 @@ import com.google.android.exoplayer2.ui.DebugTextViewHelper; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.ui.TrackSelectionView; +import com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.util.EventLogger; @@ -111,8 +107,13 @@ public class PlayerActivity extends Activity public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm"; - private static final String ABR_ALGORITHM_DEFAULT = "default"; - private static final String ABR_ALGORITHM_RANDOM = "random"; + public static final String ABR_ALGORITHM_DEFAULT = "default"; + public static final String ABR_ALGORITHM_RANDOM = "random"; + + public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode"; + public static final String SPHERICAL_STEREO_MODE_MONO = "mono"; + public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom"; + public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right"; // For backwards compatibility only. private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; @@ -123,7 +124,6 @@ public class PlayerActivity extends Activity private static final String KEY_POSITION = "position"; private static final String KEY_AUTO_PLAY = "auto_play"; - private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); private static final CookieManager DEFAULT_COOKIE_MANAGER; static { DEFAULT_COOKIE_MANAGER = new CookieManager(); @@ -134,8 +134,9 @@ public class PlayerActivity extends Activity private LinearLayout debugRootView; private TextView debugTextView; - private DataSource.Factory mediaDataSourceFactory; + private DataSource.Factory dataSourceFactory; private SimpleExoPlayer player; + private FrameworkMediaDrm mediaDrm; private MediaSource mediaSource; private DefaultTrackSelector trackSelector; private DefaultTrackSelector.Parameters trackSelectorParameters; @@ -156,8 +157,12 @@ public class PlayerActivity extends Activity @Override public void onCreate(Bundle savedInstanceState) { + String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA); + if (sphericalStereoMode != null) { + setTheme(R.style.PlayerTheme_Spherical); + } super.onCreate(savedInstanceState); - mediaDataSourceFactory = buildDataSourceFactory(true); + dataSourceFactory = buildDataSourceFactory(); if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) { CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER); } @@ -172,6 +177,21 @@ public class PlayerActivity extends Activity playerView.setControllerVisibilityListener(this); playerView.setErrorMessageProvider(new PlayerErrorMessageProvider()); playerView.requestFocus(); + if (sphericalStereoMode != null) { + int stereoMode; + if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) { + stereoMode = C.STEREO_MODE_MONO; + } else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) { + stereoMode = C.STEREO_MODE_TOP_BOTTOM; + } else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) { + stereoMode = C.STEREO_MODE_LEFT_RIGHT; + } else { + showToast(R.string.error_unrecognized_stereo_mode); + finish(); + return; + } + ((SphericalSurfaceView) playerView.getVideoSurfaceView()).setStereoMode(stereoMode); + } if (savedInstanceState != null) { trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS); @@ -187,6 +207,7 @@ public class PlayerActivity extends Activity @Override public void onNewIntent(Intent intent) { releasePlayer(); + releaseAdsLoader(); clearStartPosition(); setIntent(intent); } @@ -196,6 +217,9 @@ public class PlayerActivity extends Activity super.onStart(); if (Util.SDK_INT > 23) { initializePlayer(); + if (playerView != null) { + playerView.onResume(); + } } } @@ -204,6 +228,9 @@ public class PlayerActivity extends Activity super.onResume(); if (Util.SDK_INT <= 23 || player == null) { initializePlayer(); + if (playerView != null) { + playerView.onResume(); + } } } @@ -211,6 +238,9 @@ public class PlayerActivity extends Activity public void onPause() { super.onPause(); if (Util.SDK_INT <= 23) { + if (playerView != null) { + playerView.onPause(); + } releasePlayer(); } } @@ -219,6 +249,9 @@ public class PlayerActivity extends Activity public void onStop() { super.onStop(); if (Util.SDK_INT > 23) { + if (playerView != null) { + playerView.onPause(); + } releasePlayer(); } } @@ -327,7 +360,11 @@ public class PlayerActivity extends Activity finish(); return; } - if (Util.maybeRequestReadExternalStoragePermission(this, uris)) { + if (!Util.checkCleartextTrafficPermitted(uris)) { + showToast(R.string.error_cleartext_not_permitted); + return; + } + if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, uris)) { // The player will be reinitialized if the permission is granted. return; } @@ -368,7 +405,7 @@ public class PlayerActivity extends Activity TrackSelection.Factory trackSelectionFactory; String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA); if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) { - trackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); + trackSelectionFactory = new AdaptiveTrackSelection.Factory(); } else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) { trackSelectionFactory = new RandomTrackSelection.Factory(); } else { @@ -392,7 +429,8 @@ public class PlayerActivity extends Activity lastSeenTrackGroupArray = null; player = - ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, drmSessionManager); + ExoPlayerFactory.newSimpleInstance( + /* context= */ this, renderersFactory, trackSelector, drmSessionManager); player.addListener(new PlayerEventListener()); player.setPlayWhenReady(startAutoPlay); player.addAnalyticsListener(new EventLogger(trackSelector)); @@ -441,36 +479,29 @@ public class PlayerActivity extends Activity @ContentType int type = Util.inferContentType(uri, overrideExtension); switch (type) { case C.TYPE_DASH: - return new DashMediaSource.Factory( - new DefaultDashChunkSource.Factory(mediaDataSourceFactory), - buildDataSourceFactory(false)) + return new DashMediaSource.Factory(dataSourceFactory) .setManifestParser( - new FilteringManifestParser<>( - new DashManifestParser(), (List) getOfflineStreamKeys(uri))) + new FilteringManifestParser<>(new DashManifestParser(), getOfflineStreamKeys(uri))) .createMediaSource(uri); case C.TYPE_SS: - return new SsMediaSource.Factory( - new DefaultSsChunkSource.Factory(mediaDataSourceFactory), - buildDataSourceFactory(false)) + return new SsMediaSource.Factory(dataSourceFactory) .setManifestParser( - new FilteringManifestParser<>( - new SsManifestParser(), (List) getOfflineStreamKeys(uri))) + new FilteringManifestParser<>(new SsManifestParser(), getOfflineStreamKeys(uri))) .createMediaSource(uri); case C.TYPE_HLS: - return new HlsMediaSource.Factory(mediaDataSourceFactory) + return new HlsMediaSource.Factory(dataSourceFactory) .setPlaylistParser( - new FilteringManifestParser<>( - new HlsPlaylistParser(), (List) getOfflineStreamKeys(uri))) + new FilteringManifestParser<>(new HlsPlaylistParser(), getOfflineStreamKeys(uri))) .createMediaSource(uri); case C.TYPE_OTHER: - return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); + return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri); default: { throw new IllegalStateException("Unsupported type: " + type); } } } - private List getOfflineStreamKeys(Uri uri) { + private List getOfflineStreamKeys(Uri uri) { return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri); } @@ -478,7 +509,7 @@ public class PlayerActivity extends Activity UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession) throws UnsupportedDrmException { HttpDataSource.Factory licenseDataSourceFactory = - ((DemoApplication) getApplication()).buildHttpDataSourceFactory(/* listener= */ null); + ((DemoApplication) getApplication()).buildHttpDataSourceFactory(); HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory); if (keyRequestPropertiesArray != null) { @@ -487,8 +518,9 @@ public class PlayerActivity extends Activity keyRequestPropertiesArray[i + 1]); } } - return new DefaultDrmSessionManager<>( - uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, multiSession); + releaseMediaDrm(); + mediaDrm = FrameworkMediaDrm.newInstance(uuid); + return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession); } private void releasePlayer() { @@ -502,6 +534,23 @@ public class PlayerActivity extends Activity mediaSource = null; trackSelector = null; } + releaseMediaDrm(); + } + + private void releaseMediaDrm() { + if (mediaDrm != null) { + mediaDrm.release(); + mediaDrm = null; + } + } + + private void releaseAdsLoader() { + if (adsLoader != null) { + adsLoader.release(); + adsLoader = null; + loadedAdTagUri = null; + playerView.getOverlayFrameLayout().removeAllViews(); + } } private void updateTrackSelectorParameters() { @@ -524,16 +573,9 @@ public class PlayerActivity extends Activity startPosition = C.TIME_UNSET; } - /** - * Returns a new DataSource factory. - * - * @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new - * DataSource factory. - * @return A new DataSource factory. - */ - private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) { - return ((DemoApplication) getApplication()) - .buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null); + /** Returns a new DataSource factory. */ + private DataSource.Factory buildDataSourceFactory() { + return ((DemoApplication) getApplication()).buildDataSourceFactory(); } /** Returns an ads media source, reusing the ads loader if one exists. */ @@ -576,15 +618,6 @@ public class PlayerActivity extends Activity } } - private void releaseAdsLoader() { - if (adsLoader != null) { - adsLoader.release(); - adsLoader = null; - loadedAdTagUri = null; - playerView.getOverlayFrameLayout().removeAllViews(); - } - } - // User controls private void updateButtonVisibilities() { @@ -650,7 +683,7 @@ public class PlayerActivity extends Activity return false; } - private class PlayerEventListener extends Player.DefaultEventListener { + private class PlayerEventListener implements Player.EventListener { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 5524f98257..f683e9900f 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -24,6 +24,9 @@ import android.os.AsyncTask; import android.os.Bundle; import android.util.JsonReader; import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; @@ -55,8 +58,11 @@ public class SampleChooserActivity extends Activity private static final String TAG = "SampleChooserActivity"; + private boolean useExtensionRenderers; private DownloadTracker downloadTracker; private SampleAdapter sampleAdapter; + private MenuItem preferExtensionDecodersMenuItem; + private MenuItem randomAbrMenuItem; @Override public void onCreate(Bundle savedInstanceState) { @@ -90,13 +96,37 @@ public class SampleChooserActivity extends Activity Arrays.sort(uris); } - downloadTracker = ((DemoApplication) getApplication()).getDownloadTracker(); + DemoApplication application = (DemoApplication) getApplication(); + useExtensionRenderers = application.useExtensionRenderers(); + downloadTracker = application.getDownloadTracker(); SampleListLoader loaderTask = new SampleListLoader(); loaderTask.execute(uris); - // Ping the download service in case it's not running (but should be). - startService( - new Intent(this, DemoDownloadService.class).setAction(DownloadService.ACTION_INIT)); + // Start the download service if it should be running but it's not currently. + // Starting the service in the foreground causes notification flicker if there is no scheduled + // action. Starting it in the background throws an exception if the app is in the background too + // (e.g. if device screen is locked). + try { + DownloadService.start(this, DemoDownloadService.class); + } catch (IllegalStateException e) { + DownloadService.startForeground(this, DemoDownloadService.class); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.sample_chooser_menu, menu); + preferExtensionDecodersMenuItem = menu.findItem(R.id.prefer_extension_decoders); + preferExtensionDecodersMenuItem.setVisible(useExtensionRenderers); + randomAbrMenuItem = menu.findItem(R.id.random_abr); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + item.setChecked(!item.isChecked()); + return true; } @Override @@ -129,7 +159,13 @@ public class SampleChooserActivity extends Activity public boolean onChildClick( ExpandableListView parent, View view, int groupPosition, int childPosition, long id) { Sample sample = (Sample) view.getTag(); - startActivity(sample.buildIntent(this)); + startActivity( + sample.buildIntent( + /* context= */ this, + preferExtensionDecodersMenuItem.isChecked(), + randomAbrMenuItem.isChecked() + ? PlayerActivity.ABR_ALGORITHM_RANDOM + : PlayerActivity.ABR_ALGORITHM_DEFAULT)); return true; } @@ -239,10 +275,9 @@ public class SampleChooserActivity extends Activity String drmLicenseUrl = null; String[] drmKeyRequestProperties = null; boolean drmMultiSession = false; - boolean preferExtensionDecoders = false; ArrayList playlistSamples = null; String adTagUri = null; - String abrAlgorithm = null; + String sphericalStereoMode = null; reader.beginObject(); while (reader.hasNext()) { @@ -281,11 +316,6 @@ public class SampleChooserActivity extends Activity case "drm_multi_session": drmMultiSession = reader.nextBoolean(); break; - case "prefer_extension_decoders": - Assertions.checkState(!insidePlaylist, - "Invalid attribute on nested item: prefer_extension_decoders"); - preferExtensionDecoders = reader.nextBoolean(); - break; case "playlist": Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists"); playlistSamples = new ArrayList<>(); @@ -298,10 +328,10 @@ public class SampleChooserActivity extends Activity case "ad_tag_uri": adTagUri = reader.nextString(); break; - case "abr_algorithm": + case "spherical_stereo_mode": Assertions.checkState( - !insidePlaylist, "Invalid attribute on nested item: abr_algorithm"); - abrAlgorithm = reader.nextString(); + !insidePlaylist, "Invalid attribute on nested item: spherical_stereo_mode"); + sphericalStereoMode = reader.nextString(); break; default: throw new ParserException("Unsupported attribute name: " + name); @@ -315,11 +345,15 @@ public class SampleChooserActivity extends Activity if (playlistSamples != null) { UriSample[] playlistSamplesArray = playlistSamples.toArray( new UriSample[playlistSamples.size()]); - return new PlaylistSample( - sampleName, preferExtensionDecoders, abrAlgorithm, drmInfo, playlistSamplesArray); + return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray); } else { return new UriSample( - sampleName, preferExtensionDecoders, abrAlgorithm, drmInfo, uri, extension, adTagUri); + sampleName, + drmInfo, + uri, + extension, + adTagUri, + sphericalStereoMode); } } @@ -477,19 +511,15 @@ public class SampleChooserActivity extends Activity private abstract static class Sample { public final String name; - public final boolean preferExtensionDecoders; - public final String abrAlgorithm; public final DrmInfo drmInfo; - public Sample( - String name, boolean preferExtensionDecoders, String abrAlgorithm, DrmInfo drmInfo) { + public Sample(String name, DrmInfo drmInfo) { this.name = name; - this.preferExtensionDecoders = preferExtensionDecoders; - this.abrAlgorithm = abrAlgorithm; this.drmInfo = drmInfo; } - public Intent buildIntent(Context context) { + public Intent buildIntent( + Context context, boolean preferExtensionDecoders, String abrAlgorithm) { Intent intent = new Intent(context, PlayerActivity.class); intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders); intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm); @@ -506,27 +536,30 @@ public class SampleChooserActivity extends Activity public final Uri uri; public final String extension; public final String adTagUri; + public final String sphericalStereoMode; public UriSample( String name, - boolean preferExtensionDecoders, - String abrAlgorithm, DrmInfo drmInfo, Uri uri, String extension, - String adTagUri) { - super(name, preferExtensionDecoders, abrAlgorithm, drmInfo); + String adTagUri, + String sphericalStereoMode) { + super(name, drmInfo); this.uri = uri; this.extension = extension; this.adTagUri = adTagUri; + this.sphericalStereoMode = sphericalStereoMode; } @Override - public Intent buildIntent(Context context) { - return super.buildIntent(context) + public Intent buildIntent( + Context context, boolean preferExtensionDecoders, String abrAlgorithm) { + return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm) .setData(uri) .putExtra(PlayerActivity.EXTENSION_EXTRA, extension) .putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri) + .putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode) .setAction(PlayerActivity.ACTION_VIEW); } @@ -538,23 +571,22 @@ public class SampleChooserActivity extends Activity public PlaylistSample( String name, - boolean preferExtensionDecoders, - String abrAlgorithm, DrmInfo drmInfo, UriSample... children) { - super(name, preferExtensionDecoders, abrAlgorithm, drmInfo); + super(name, drmInfo); this.children = children; } @Override - public Intent buildIntent(Context context) { + public Intent buildIntent( + Context context, boolean preferExtensionDecoders, String abrAlgorithm) { String[] uris = new String[children.length]; String[] extensions = new String[children.length]; for (int i = 0; i < children.length; i++) { uris[i] = children[i].uri.toString(); extensions[i] = children[i].extension; } - return super.buildIntent(context) + return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm) .putExtra(PlayerActivity.URI_LIST_EXTRA, uris) .putExtra(PlayerActivity.EXTENSION_LIST_EXTRA, extensions) .setAction(PlayerActivity.ACTION_VIEW_LIST); diff --git a/demos/main/src/main/res/menu/sample_chooser_menu.xml b/demos/main/src/main/res/menu/sample_chooser_menu.xml new file mode 100644 index 0000000000..566b23a0d5 --- /dev/null +++ b/demos/main/src/main/res/menu/sample_chooser_menu.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml index eb260e6ffc..40f065b18e 100644 --- a/demos/main/src/main/res/values/strings.xml +++ b/demos/main/src/main/res/values/strings.xml @@ -19,10 +19,14 @@ Unexpected intent action: %1$s + Cleartext traffic not permitted + Playback failed Unrecognized ABR algorithm + Unrecognized stereo mode + Protected content not supported on API levels below 18 This device does not support the required DRM scheme @@ -57,4 +61,8 @@ IMA does not support offline ads + Prefer extension decoders + + Enable random ABR + diff --git a/demos/main/src/main/res/values/styles.xml b/demos/main/src/main/res/values/styles.xml index 5616bb9869..25d826bdf6 100644 --- a/demos/main/src/main/res/values/styles.xml +++ b/demos/main/src/main/res/values/styles.xml @@ -20,4 +20,8 @@ @android:color/black + + diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index ded92000d3..35499f1c1d 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion 14 targetSdkVersion project.ext.targetSdkVersion @@ -26,17 +31,7 @@ android { } dependencies { - // These dependencies are necessary to force the supportLibraryVersion of - // com.android.support:support-v4, com.android.support:appcompat-v7 and - // com.android.support:mediarouter-v7 to be used. Else older versions are - // used, for example: - // com.google.android.gms:play-services-cast-framework:12.0.0 - // |-- com.google.android.gms:play-services-basement:12.0.0 - // |-- com.android.support:support-v4:26.1.0 - api 'com.android.support:support-v4:' + supportLibraryVersion - api 'com.android.support:appcompat-v7:' + supportLibraryVersion - api 'com.android.support:mediarouter-v7:' + supportLibraryVersion - api 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion + api 'com.google.android.gms:play-services-cast-framework:16.0.1' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') testImplementation project(modulePrefix + 'testutils') @@ -44,8 +39,19 @@ dependencies { testImplementation 'org.mockito:mockito-core:' + mockitoVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation project(modulePrefix + 'testutils-robolectric') + // These dependencies are necessary to force the supportLibraryVersion of + // com.android.support:support-v4, com.android.support:appcompat-v7 and + // com.android.support:mediarouter-v7 to be used. Else older versions are + // used, for example via: + // com.google.android.gms:play-services-cast-framework:15.0.1 + // |-- com.android.support:mediarouter-v7:26.1.0 + api 'com.android.support:support-v4:' + supportLibraryVersion + api 'com.android.support:mediarouter-v7:' + supportLibraryVersion + api 'com.android.support:recyclerview-v7:' + supportLibraryVersion } +apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' + ext { javadocTitle = 'Cast extension' } diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 84724cbb47..21e853dd62 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -283,6 +283,11 @@ public final class CastPlayer implements Player { // Player implementation. + @Override + public AudioComponent getAudioComponent() { + return null; + } + @Override public VideoComponent getVideoComponent() { return null; @@ -526,6 +531,15 @@ public final class CastPlayer implements Player { : duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100); } + @Override + public long getTotalBufferedDuration() { + long bufferedPosition = getBufferedPosition(); + long currentPosition = getCurrentPosition(); + return bufferedPosition == C.TIME_UNSET || currentPosition == C.TIME_UNSET + ? 0 + : bufferedPosition - currentPosition; + } + @Override public boolean isCurrentWindowDynamic() { return !currentTimeline.isEmpty() @@ -563,6 +577,11 @@ public final class CastPlayer implements Player { return getCurrentPosition(); } + @Override + public long getContentBufferedPosition() { + return getBufferedPosition(); + } + // Internal methods. public void updateInternalState() { diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java index 396f6f8769..4939e62a2b 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java @@ -32,8 +32,7 @@ import java.util.Map; /* package */ final class CastTimeline extends Timeline { public static final CastTimeline EMPTY_CAST_TIMELINE = - new CastTimeline( - Collections.emptyList(), Collections.emptyMap()); + new CastTimeline(Collections.emptyList(), Collections.emptyMap()); private final SparseIntArray idsToIndex; private final int[] ids; @@ -108,6 +107,11 @@ import java.util.Map; return uid instanceof Integer ? idsToIndex.get((int) uid, C.INDEX_UNSET) : C.INDEX_UNSET; } + @Override + public Object getUidOfPeriod(int periodIndex) { + return ids[periodIndex]; + } + // equals and hashCode implementations. @Override diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java index d2154eec1b..997857f6b5 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java @@ -101,8 +101,15 @@ import com.google.android.gms.cast.MediaTrack; * @return The equivalent {@link Format}. */ public static Format mediaTrackToFormat(MediaTrack mediaTrack) { - return Format.createContainerFormat(mediaTrack.getContentId(), mediaTrack.getContentType(), - null, null, Format.NO_VALUE, 0, mediaTrack.getLanguage()); + return Format.createContainerFormat( + mediaTrack.getContentId(), + /* label= */ null, + mediaTrack.getContentType(), + /* sampleMimeType= */ null, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* selectionFlags= */ 0, + mediaTrack.getLanguage()); } private CastUtils() {} diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md index ea84b602db..f1f6d68c81 100644 --- a/extensions/cronet/README.md +++ b/extensions/cronet/README.md @@ -5,37 +5,22 @@ The Cronet extension is an [HttpDataSource][] implementation using [Cronet][]. [HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html [Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F -## Build instructions ## +## Getting the extension ## -To use this extension you need to clone the ExoPlayer repository and depend on -its modules locally. Instructions for doing this can be found in ExoPlayer's -[top level README][]. In addition, it's necessary to get the Cronet libraries -and enable the extension: +The easiest way to use the extension is to add it as a gradle dependency: -1. Find the latest Cronet release [here][] and navigate to its `Release/cronet` - directory -1. Download `cronet_api.jar`, `cronet_impl_common_java.jar`, - `cronet_impl_native_java.jar` and the `libs` directory -1. Copy the three jar files into the `libs` directory of this extension -1. Copy the content of the downloaded `libs` directory into the `jniLibs` - directory of this extension -1. In your `settings.gradle` file, add - `gradle.ext.exoplayerIncludeCronetExtension = true` before the line that - applies `core_settings.gradle`. -1. In all `build.gradle` files where this extension is linked as a dependency, - add - ``` - android { - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - } - ``` - to enable Java 8 features required by the Cronet library. +```gradle +implementation 'com.google.android.exoplayer:extension-cronet:2.X.X' +``` + +where `2.X.X` is the version, which must match the version of the ExoPlayer +library being used. + +Alternatively, you can clone the ExoPlayer repository and depend on the module +locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md -[here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android ## Using the extension ## diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 0a52344464..7d8c217b58 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -19,14 +19,10 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion project.ext.minSdkVersion + minSdkVersion 16 targetSdkVersion project.ext.targetSdkVersion } - sourceSets.main { - jniLibs.srcDirs = ['jniLibs'] - } - compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -34,9 +30,7 @@ android { } dependencies { - api files('libs/cronet_api.jar') - implementation files('libs/cronet_impl_common_java.jar') - implementation files('libs/cronet_impl_native_java.jar') + api 'org.chromium.net:cronet-embedded:66.3359.158' implementation project(modulePrefix + 'library-core') implementation 'com.android.support:support-annotations:' + supportLibraryVersion testImplementation project(modulePrefix + 'library') @@ -47,3 +41,9 @@ ext { javadocTitle = 'Cronet extension' } apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-cronet' + releaseDescription = 'Cronet extension for ExoPlayer.' +} +apply from: '../../publish.gradle' diff --git a/extensions/cronet/jniLibs/README.md b/extensions/cronet/jniLibs/README.md deleted file mode 100644 index e9f0717ae6..0000000000 --- a/extensions/cronet/jniLibs/README.md +++ /dev/null @@ -1 +0,0 @@ -Copy folders containing architecture specific .so files here. diff --git a/extensions/cronet/libs/README.md b/extensions/cronet/libs/README.md deleted file mode 100644 index 641a80db18..0000000000 --- a/extensions/cronet/libs/README.md +++ /dev/null @@ -1 +0,0 @@ -Copy cronet.jar and cronet_api.jar here. diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index db980aa72b..fd6a3ce9ec 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -20,10 +20,10 @@ import android.text.TextUtils; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.upstream.BaseDataSource; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; -import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.ConditionVariable; @@ -32,6 +32,7 @@ import java.io.IOException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.nio.ByteBuffer; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -47,9 +48,10 @@ import org.chromium.net.UrlResponseInfo; /** * DataSource without intermediate buffer based on Cronet API set using UrlRequest. + * *

This class's methods are organized in the sequence of expected calls. */ -public class CronetDataSource extends UrlRequest.Callback implements HttpDataSource { +public class CronetDataSource extends BaseDataSource implements HttpDataSource { /** * Thrown when an error is encountered when trying to open a {@link CronetDataSource}. @@ -95,6 +97,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou */ public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000; + /* package */ final UrlRequest.Callback urlRequestCallback; + private static final String TAG = "CronetDataSource"; private static final String CONTENT_TYPE = "Content-Type"; private static final String SET_COOKIE = "Set-Cookie"; @@ -108,7 +112,6 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou private final CronetEngine cronetEngine; private final Executor executor; private final Predicate contentTypePredicate; - private final TransferListener listener; private final int connectTimeoutMs; private final int readTimeoutMs; private final boolean resetTimeoutOnRedirects; @@ -143,57 +146,73 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou /** * @param cronetEngine A CronetEngine. - * @param executor The {@link java.util.concurrent.Executor} that will handle responses. - * This may be a direct executor (i.e. executes tasks on the calling thread) in order - * to avoid a thread hop from Cronet's internal network thread to the response handling - * thread. However, to avoid slowing down overall network performance, care must be taken - * to make sure response handling is a fast operation when using a direct executor. + * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may + * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread + * hop from Cronet's internal network thread to the response handling thread. However, to + * avoid slowing down overall network performance, care must be taken to make sure response + * handling is a fast operation when using a direct executor. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. - * @param listener An optional listener. + * predicate then an {@link InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. */ - public CronetDataSource(CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, TransferListener listener) { - this(cronetEngine, executor, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS, - DEFAULT_READ_TIMEOUT_MILLIS, false, null, false); + public CronetDataSource( + CronetEngine cronetEngine, Executor executor, Predicate contentTypePredicate) { + this( + cronetEngine, + executor, + contentTypePredicate, + DEFAULT_CONNECT_TIMEOUT_MILLIS, + DEFAULT_READ_TIMEOUT_MILLIS, + false, + null, + false); } /** * @param cronetEngine A CronetEngine. - * @param executor The {@link java.util.concurrent.Executor} that will handle responses. - * This may be a direct executor (i.e. executes tasks on the calling thread) in order - * to avoid a thread hop from Cronet's internal network thread to the response handling - * thread. However, to avoid slowing down overall network performance, care must be taken - * to make sure response handling is a fast operation when using a direct executor. + * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may + * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread + * hop from Cronet's internal network thread to the response handling thread. However, to + * avoid slowing down overall network performance, care must be taken to make sure response + * handling is a fast operation when using a direct executor. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. - * @param listener An optional listener. + * predicate then an {@link InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. * @param connectTimeoutMs The connection timeout, in milliseconds. * @param readTimeoutMs The read timeout, in milliseconds. * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. * @param defaultRequestProperties The default request properties to be used. */ - public CronetDataSource(CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, TransferListener listener, - int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, + public CronetDataSource( + CronetEngine cronetEngine, + Executor executor, + Predicate contentTypePredicate, + int connectTimeoutMs, + int readTimeoutMs, + boolean resetTimeoutOnRedirects, RequestProperties defaultRequestProperties) { - this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs, - readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties, false); + this( + cronetEngine, + executor, + contentTypePredicate, + connectTimeoutMs, + readTimeoutMs, + resetTimeoutOnRedirects, + Clock.DEFAULT, + defaultRequestProperties, + false); } /** * @param cronetEngine A CronetEngine. - * @param executor The {@link java.util.concurrent.Executor} that will handle responses. - * This may be a direct executor (i.e. executes tasks on the calling thread) in order - * to avoid a thread hop from Cronet's internal network thread to the response handling - * thread. However, to avoid slowing down overall network performance, care must be taken - * to make sure response handling is a fast operation when using a direct executor. + * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may + * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread + * hop from Cronet's internal network thread to the response handling thread. However, to + * avoid slowing down overall network performance, care must be taken to make sure response + * handling is a fast operation when using a direct executor. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. - * @param listener An optional listener. + * predicate then an {@link InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. * @param connectTimeoutMs The connection timeout, in milliseconds. * @param readTimeoutMs The read timeout, in milliseconds. * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. @@ -201,23 +220,42 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou * @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to * the redirect url in the "Cookie" header. */ - public CronetDataSource(CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, TransferListener listener, - int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, - RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) { - this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs, - readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties, + public CronetDataSource( + CronetEngine cronetEngine, + Executor executor, + Predicate contentTypePredicate, + int connectTimeoutMs, + int readTimeoutMs, + boolean resetTimeoutOnRedirects, + RequestProperties defaultRequestProperties, + boolean handleSetCookieRequests) { + this( + cronetEngine, + executor, + contentTypePredicate, + connectTimeoutMs, + readTimeoutMs, + resetTimeoutOnRedirects, + Clock.DEFAULT, + defaultRequestProperties, handleSetCookieRequests); } - /* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, TransferListener listener, - int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock, - RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) { + /* package */ CronetDataSource( + CronetEngine cronetEngine, + Executor executor, + Predicate contentTypePredicate, + int connectTimeoutMs, + int readTimeoutMs, + boolean resetTimeoutOnRedirects, + Clock clock, + RequestProperties defaultRequestProperties, + boolean handleSetCookieRequests) { + super(/* isNetwork= */ true); + this.urlRequestCallback = new UrlRequestCallback(); this.cronetEngine = Assertions.checkNotNull(cronetEngine); this.executor = Assertions.checkNotNull(executor); this.contentTypePredicate = contentTypePredicate; - this.listener = listener; this.connectTimeoutMs = connectTimeoutMs; this.readTimeoutMs = readTimeoutMs; this.resetTimeoutOnRedirects = resetTimeoutOnRedirects; @@ -247,7 +285,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou @Override public Map> getResponseHeaders() { - return responseInfo == null ? null : responseInfo.getAllHeaders(); + return responseInfo == null ? Collections.emptyMap() : responseInfo.getAllHeaders(); } @Override @@ -270,6 +308,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou } currentUrlRequest.start(); + transferInitializing(dataSpec); try { boolean connectionOpened = blockUntilConnectTimeout(); if (exception != null) { @@ -323,9 +362,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesRemaining; } @@ -391,9 +428,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= bytesRead; } - if (listener != null) { - listener.onBytesTransferred(this, bytesRead); - } + bytesTransferred(bytesRead); return bytesRead; } @@ -412,107 +447,17 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou finished = false; if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } - // UrlRequest.Callback implementation - - @Override - public synchronized void onRedirectReceived(UrlRequest request, UrlResponseInfo info, - String newLocationUrl) { - if (request != currentUrlRequest) { - return; - } - if (currentDataSpec.postBody != null) { - int responseCode = info.getHttpStatusCode(); - // The industry standard is to disregard POST redirects when the status code is 307 or 308. - // For other redirect response codes the POST request is converted to a GET request and the - // redirect is followed. - if (responseCode == 307 || responseCode == 308) { - exception = new InvalidResponseCodeException(responseCode, info.getAllHeaders(), - currentDataSpec); - operation.open(); - return; - } - } - if (resetTimeoutOnRedirects) { - resetConnectTimeout(); - } - - Map> headers = info.getAllHeaders(); - if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) { - request.followRedirect(); - } else { - currentUrlRequest.cancel(); - DataSpec redirectUrlDataSpec = new DataSpec(Uri.parse(newLocationUrl), - currentDataSpec.postBody, currentDataSpec.absoluteStreamPosition, - currentDataSpec.position, currentDataSpec.length, currentDataSpec.key, - currentDataSpec.flags); - UrlRequest.Builder requestBuilder; - try { - requestBuilder = buildRequestBuilder(redirectUrlDataSpec); - } catch (IOException e) { - exception = e; - return; - } - String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE)); - attachCookies(requestBuilder, cookieHeadersValue); - currentUrlRequest = requestBuilder.build(); - currentUrlRequest.start(); - } - } - - @Override - public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) { - if (request != currentUrlRequest) { - return; - } - responseInfo = info; - operation.open(); - } - - @Override - public synchronized void onReadCompleted(UrlRequest request, UrlResponseInfo info, - ByteBuffer buffer) { - if (request != currentUrlRequest) { - return; - } - operation.open(); - } - - @Override - public synchronized void onSucceeded(UrlRequest request, UrlResponseInfo info) { - if (request != currentUrlRequest) { - return; - } - finished = true; - operation.open(); - } - - @Override - public synchronized void onFailed(UrlRequest request, UrlResponseInfo info, - CronetException error) { - if (request != currentUrlRequest) { - return; - } - if (error instanceof NetworkException - && ((NetworkException) error).getErrorCode() - == NetworkException.ERROR_HOSTNAME_NOT_RESOLVED) { - exception = new UnknownHostException(); - } else { - exception = error; - } - operation.open(); - } - // Internal methods. private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException { - UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder( - dataSpec.uri.toString(), this, executor).allowDirectExecutor(); + UrlRequest.Builder requestBuilder = + cronetEngine + .newUrlRequestBuilder(dataSpec.uri.toString(), urlRequestCallback, executor) + .allowDirectExecutor(); // Set the headers. boolean isContentTypeHeaderSet = false; if (defaultRequestProperties != null) { @@ -528,8 +473,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key); requestBuilder.addHeader(key, headerEntry.getValue()); } - if (dataSpec.postBody != null && dataSpec.postBody.length != 0 && !isContentTypeHeaderSet) { - throw new IOException("POST request with non-empty body must set Content-Type"); + if (dataSpec.httpBody != null && !isContentTypeHeaderSet) { + throw new IOException("HTTP request with non-empty body must set Content-Type"); } // Set the Range header. if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) { @@ -549,12 +494,10 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou // requestBuilder.addHeader("Accept-Encoding", "identity"); // } // Set the method and (if non-empty) the body. - if (dataSpec.postBody != null) { - requestBuilder.setHttpMethod("POST"); - if (dataSpec.postBody.length != 0) { - requestBuilder.setUploadDataProvider(new ByteArrayUploadDataProvider(dataSpec.postBody), - executor); - } + requestBuilder.setHttpMethod(dataSpec.getHttpMethodString()); + if (dataSpec.httpBody != null) { + requestBuilder.setUploadDataProvider( + new ByteArrayUploadDataProvider(dataSpec.httpBody), executor); } return requestBuilder; } @@ -655,4 +598,91 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou return list == null || list.isEmpty(); } + private final class UrlRequestCallback extends UrlRequest.Callback { + + @Override + public synchronized void onRedirectReceived( + UrlRequest request, UrlResponseInfo info, String newLocationUrl) { + if (request != currentUrlRequest) { + return; + } + if (currentDataSpec.postBody != null) { + int responseCode = info.getHttpStatusCode(); + // The industry standard is to disregard POST redirects when the status code is 307 or 308. + // For other redirect response codes the POST request is converted to a GET request and the + // redirect is followed. + if (responseCode == 307 || responseCode == 308) { + exception = + new InvalidResponseCodeException(responseCode, info.getAllHeaders(), currentDataSpec); + operation.open(); + return; + } + } + if (resetTimeoutOnRedirects) { + resetConnectTimeout(); + } + + Map> headers = info.getAllHeaders(); + if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) { + request.followRedirect(); + } else { + currentUrlRequest.cancel(); + DataSpec redirectUrlDataSpec = currentDataSpec.withUri(Uri.parse(newLocationUrl)); + UrlRequest.Builder requestBuilder; + try { + requestBuilder = buildRequestBuilder(redirectUrlDataSpec); + } catch (IOException e) { + exception = e; + return; + } + String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE)); + attachCookies(requestBuilder, cookieHeadersValue); + currentUrlRequest = requestBuilder.build(); + currentUrlRequest.start(); + } + } + + @Override + public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) { + if (request != currentUrlRequest) { + return; + } + responseInfo = info; + operation.open(); + } + + @Override + public synchronized void onReadCompleted( + UrlRequest request, UrlResponseInfo info, ByteBuffer buffer) { + if (request != currentUrlRequest) { + return; + } + operation.open(); + } + + @Override + public synchronized void onSucceeded(UrlRequest request, UrlResponseInfo info) { + if (request != currentUrlRequest) { + return; + } + finished = true; + operation.open(); + } + + @Override + public synchronized void onFailed( + UrlRequest request, UrlResponseInfo info, CronetException error) { + if (request != currentUrlRequest) { + return; + } + if (error instanceof NetworkException + && ((NetworkException) error).getErrorCode() + == NetworkException.ERROR_HOSTNAME_NOT_RESOLVED) { + exception = new UnknownHostException(); + } else { + exception = error; + } + operation.open(); + } + } } diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java index d6237fc988..d832e4625d 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer2.ext.cronet; -import com.google.android.exoplayer2.upstream.DataSource; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; @@ -46,7 +46,7 @@ public final class CronetDataSourceFactory extends BaseFactory { private final CronetEngineWrapper cronetEngineWrapper; private final Executor executor; private final Predicate contentTypePredicate; - private final TransferListener transferListener; + private final @Nullable TransferListener transferListener; private final int connectTimeoutMs; private final int readTimeoutMs; private final boolean resetTimeoutOnRedirects; @@ -54,26 +54,176 @@ public final class CronetDataSourceFactory extends BaseFactory { /** * Constructs a CronetDataSourceFactory. - *

- * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided + * + *

If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided * fallback {@link HttpDataSource.Factory} will be used instead. * - * Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link - * CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables + *

Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, + * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables * cross-protocol redirects. * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from - * {@link CronetDataSource#open}. - * @param transferListener An optional listener. - * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case - * no suitable CronetEngine can be build. + * predicate then an {@link InvalidContentTypeException} is thrown from {@link + * CronetDataSource#open}. + * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no + * suitable CronetEngine can be build. */ - public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper, - Executor executor, Predicate contentTypePredicate, - TransferListener transferListener, + public CronetDataSourceFactory( + CronetEngineWrapper cronetEngineWrapper, + Executor executor, + Predicate contentTypePredicate, + HttpDataSource.Factory fallbackFactory) { + this( + cronetEngineWrapper, + executor, + contentTypePredicate, + /* transferListener= */ null, + DEFAULT_CONNECT_TIMEOUT_MILLIS, + DEFAULT_READ_TIMEOUT_MILLIS, + false, + fallbackFactory); + } + + /** + * Constructs a CronetDataSourceFactory. + * + *

If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link + * DefaultHttpDataSourceFactory} will be used instead. + * + *

Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, + * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables + * cross-protocol redirects. + * + * @param cronetEngineWrapper A {@link CronetEngineWrapper}. + * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. + * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the + * predicate then an {@link InvalidContentTypeException} is thrown from {@link + * CronetDataSource#open}. + * @param userAgent A user agent used to create a fallback HttpDataSource if needed. + */ + public CronetDataSourceFactory( + CronetEngineWrapper cronetEngineWrapper, + Executor executor, + Predicate contentTypePredicate, + String userAgent) { + this( + cronetEngineWrapper, + executor, + contentTypePredicate, + /* transferListener= */ null, + DEFAULT_CONNECT_TIMEOUT_MILLIS, + DEFAULT_READ_TIMEOUT_MILLIS, + false, + new DefaultHttpDataSourceFactory( + userAgent, + /* listener= */ null, + DEFAULT_CONNECT_TIMEOUT_MILLIS, + DEFAULT_READ_TIMEOUT_MILLIS, + false)); + } + + /** + * Constructs a CronetDataSourceFactory. + * + *

If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link + * DefaultHttpDataSourceFactory} will be used instead. + * + * @param cronetEngineWrapper A {@link CronetEngineWrapper}. + * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. + * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the + * predicate then an {@link InvalidContentTypeException} is thrown from {@link + * CronetDataSource#open}. + * @param connectTimeoutMs The connection timeout, in milliseconds. + * @param readTimeoutMs The read timeout, in milliseconds. + * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. + * @param userAgent A user agent used to create a fallback HttpDataSource if needed. + */ + public CronetDataSourceFactory( + CronetEngineWrapper cronetEngineWrapper, + Executor executor, + Predicate contentTypePredicate, + int connectTimeoutMs, + int readTimeoutMs, + boolean resetTimeoutOnRedirects, + String userAgent) { + this( + cronetEngineWrapper, + executor, + contentTypePredicate, + /* transferListener= */ null, + DEFAULT_CONNECT_TIMEOUT_MILLIS, + DEFAULT_READ_TIMEOUT_MILLIS, + resetTimeoutOnRedirects, + new DefaultHttpDataSourceFactory( + userAgent, + /* listener= */ null, + connectTimeoutMs, + readTimeoutMs, + resetTimeoutOnRedirects)); + } + + /** + * Constructs a CronetDataSourceFactory. + * + *

If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided + * fallback {@link HttpDataSource.Factory} will be used instead. + * + * @param cronetEngineWrapper A {@link CronetEngineWrapper}. + * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. + * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the + * predicate then an {@link InvalidContentTypeException} is thrown from {@link + * CronetDataSource#open}. + * @param connectTimeoutMs The connection timeout, in milliseconds. + * @param readTimeoutMs The read timeout, in milliseconds. + * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. + * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no + * suitable CronetEngine can be build. + */ + public CronetDataSourceFactory( + CronetEngineWrapper cronetEngineWrapper, + Executor executor, + Predicate contentTypePredicate, + int connectTimeoutMs, + int readTimeoutMs, + boolean resetTimeoutOnRedirects, + HttpDataSource.Factory fallbackFactory) { + this( + cronetEngineWrapper, + executor, + contentTypePredicate, + /* transferListener= */ null, + connectTimeoutMs, + readTimeoutMs, + resetTimeoutOnRedirects, + fallbackFactory); + } + + /** + * Constructs a CronetDataSourceFactory. + * + *

If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided + * fallback {@link HttpDataSource.Factory} will be used instead. + * + *

Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, + * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables + * cross-protocol redirects. + * + * @param cronetEngineWrapper A {@link CronetEngineWrapper}. + * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. + * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the + * predicate then an {@link InvalidContentTypeException} is thrown from {@link + * CronetDataSource#open}. + * @param transferListener An optional listener. + * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no + * suitable CronetEngine can be build. + */ + public CronetDataSourceFactory( + CronetEngineWrapper cronetEngineWrapper, + Executor executor, + Predicate contentTypePredicate, + @Nullable TransferListener transferListener, HttpDataSource.Factory fallbackFactory) { this(cronetEngineWrapper, executor, contentTypePredicate, transferListener, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory); @@ -81,25 +231,28 @@ public final class CronetDataSourceFactory extends BaseFactory { /** * Constructs a CronetDataSourceFactory. - *

- * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a - * {@link DefaultHttpDataSourceFactory} will be used instead. * - * Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link - * CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables + *

If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link + * DefaultHttpDataSourceFactory} will be used instead. + * + *

Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, + * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables * cross-protocol redirects. * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from - * {@link CronetDataSource#open}. + * predicate then an {@link InvalidContentTypeException} is thrown from {@link + * CronetDataSource#open}. * @param transferListener An optional listener. * @param userAgent A user agent used to create a fallback HttpDataSource if needed. */ - public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper, - Executor executor, Predicate contentTypePredicate, - TransferListener transferListener, String userAgent) { + public CronetDataSourceFactory( + CronetEngineWrapper cronetEngineWrapper, + Executor executor, + Predicate contentTypePredicate, + @Nullable TransferListener transferListener, + String userAgent) { this(cronetEngineWrapper, executor, contentTypePredicate, transferListener, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, new DefaultHttpDataSourceFactory(userAgent, transferListener, @@ -108,25 +261,30 @@ public final class CronetDataSourceFactory extends BaseFactory { /** * Constructs a CronetDataSourceFactory. - *

- * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a - * {@link DefaultHttpDataSourceFactory} will be used instead. + * + *

If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link + * DefaultHttpDataSourceFactory} will be used instead. * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from - * {@link CronetDataSource#open}. + * predicate then an {@link InvalidContentTypeException} is thrown from {@link + * CronetDataSource#open}. * @param transferListener An optional listener. * @param connectTimeoutMs The connection timeout, in milliseconds. * @param readTimeoutMs The read timeout, in milliseconds. * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. * @param userAgent A user agent used to create a fallback HttpDataSource if needed. */ - public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper, - Executor executor, Predicate contentTypePredicate, - TransferListener transferListener, int connectTimeoutMs, - int readTimeoutMs, boolean resetTimeoutOnRedirects, String userAgent) { + public CronetDataSourceFactory( + CronetEngineWrapper cronetEngineWrapper, + Executor executor, + Predicate contentTypePredicate, + @Nullable TransferListener transferListener, + int connectTimeoutMs, + int readTimeoutMs, + boolean resetTimeoutOnRedirects, + String userAgent) { this(cronetEngineWrapper, executor, contentTypePredicate, transferListener, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects, new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs, @@ -135,26 +293,30 @@ public final class CronetDataSourceFactory extends BaseFactory { /** * Constructs a CronetDataSourceFactory. - *

- * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided + * + *

If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided * fallback {@link HttpDataSource.Factory} will be used instead. * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from - * {@link CronetDataSource#open}. + * predicate then an {@link InvalidContentTypeException} is thrown from {@link + * CronetDataSource#open}. * @param transferListener An optional listener. * @param connectTimeoutMs The connection timeout, in milliseconds. * @param readTimeoutMs The read timeout, in milliseconds. * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. - * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case - * no suitable CronetEngine can be build. + * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no + * suitable CronetEngine can be build. */ - public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper, - Executor executor, Predicate contentTypePredicate, - TransferListener transferListener, int connectTimeoutMs, - int readTimeoutMs, boolean resetTimeoutOnRedirects, + public CronetDataSourceFactory( + CronetEngineWrapper cronetEngineWrapper, + Executor executor, + Predicate contentTypePredicate, + @Nullable TransferListener transferListener, + int connectTimeoutMs, + int readTimeoutMs, + boolean resetTimeoutOnRedirects, HttpDataSource.Factory fallbackFactory) { this.cronetEngineWrapper = cronetEngineWrapper; this.executor = executor; @@ -173,8 +335,19 @@ public final class CronetDataSourceFactory extends BaseFactory { if (cronetEngine == null) { return fallbackFactory.createDataSource(); } - return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener, - connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties); + CronetDataSource dataSource = + new CronetDataSource( + cronetEngine, + executor, + contentTypePredicate, + connectTimeoutMs, + readTimeoutMs, + resetTimeoutOnRedirects, + defaultRequestProperties); + if (transferListener != null) { + dataSource.addTransferListener(transferListener); + } + return dataSource; } } diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index 4e990cd027..3e2242826c 100644 --- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -24,7 +24,6 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -81,13 +80,14 @@ public final class CronetDataSourceTest { private DataSpec testDataSpec; private DataSpec testPostDataSpec; + private DataSpec testHeadDataSpec; private Map testResponseHeader; private UrlResponseInfo testUrlResponseInfo; @Mock private UrlRequest.Builder mockUrlRequestBuilder; @Mock private UrlRequest mockUrlRequest; @Mock private Predicate mockContentTypePredicate; - @Mock private TransferListener mockTransferListener; + @Mock private TransferListener mockTransferListener; @Mock private Executor mockExecutor; @Mock private NetworkException mockNetworkException; @Mock private CronetEngine mockCronetEngine; @@ -99,18 +99,17 @@ public final class CronetDataSourceTest { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); dataSourceUnderTest = - spy( - new CronetDataSource( - mockCronetEngine, - mockExecutor, - mockContentTypePredicate, - mockTransferListener, - TEST_CONNECT_TIMEOUT_MS, - TEST_READ_TIMEOUT_MS, - true, // resetTimeoutOnRedirects - Clock.DEFAULT, - null, - false)); + new CronetDataSource( + mockCronetEngine, + mockExecutor, + mockContentTypePredicate, + TEST_CONNECT_TIMEOUT_MS, + TEST_READ_TIMEOUT_MS, + true, // resetTimeoutOnRedirects + Clock.DEFAULT, + null, + false); + dataSourceUnderTest.addTransferListener(mockTransferListener); when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true); when(mockCronetEngine.newUrlRequestBuilder( anyString(), any(UrlRequest.Callback.class), any(Executor.class))) @@ -122,6 +121,9 @@ public final class CronetDataSourceTest { testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null); testPostDataSpec = new DataSpec(Uri.parse(TEST_URL), TEST_POST_BODY, 0, 0, C.LENGTH_UNSET, null, 0); + testHeadDataSpec = + new DataSpec( + Uri.parse(TEST_URL), DataSpec.HTTP_METHOD_HEAD, null, 0, 0, C.LENGTH_UNSET, null, 0); testResponseHeader = new HashMap<>(); testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE); // This value can be anything since the DataSpec is unset. @@ -172,9 +174,10 @@ public final class CronetDataSourceTest { @Override public Object answer(InvocationOnMock invocation) throws Throwable { // Invoke the callback for the previous request. - dataSourceUnderTest.onFailed( + dataSourceUnderTest.urlRequestCallback.onFailed( mockUrlRequest, testUrlResponseInfo, mockNetworkException); - dataSourceUnderTest.onResponseStarted(mockUrlRequest2, testUrlResponseInfo); + dataSourceUnderTest.urlRequestCallback.onResponseStarted( + mockUrlRequest2, testUrlResponseInfo); return null; } }) @@ -213,7 +216,8 @@ public final class CronetDataSourceTest { public void testRequestOpen() throws HttpDataSourceException { mockResponseStartSuccess(); assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(TEST_CONTENT_LENGTH); - verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec); + verify(mockTransferListener) + .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true); } @Test @@ -225,7 +229,8 @@ public final class CronetDataSourceTest { mockResponseStartSuccess(); assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(5000 /* contentLength */); - verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec); + verify(mockTransferListener) + .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true); } @Test @@ -239,7 +244,8 @@ public final class CronetDataSourceTest { // Check for connection not automatically closed. assertThat(e.getCause() instanceof UnknownHostException).isFalse(); verify(mockUrlRequest, never()).cancel(); - verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec); + verify(mockTransferListener, never()) + .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true); } } @@ -256,7 +262,8 @@ public final class CronetDataSourceTest { // Check for connection not automatically closed. assertThat(e.getCause() instanceof UnknownHostException).isTrue(); verify(mockUrlRequest, never()).cancel(); - verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec); + verify(mockTransferListener, never()) + .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true); } } @@ -272,7 +279,8 @@ public final class CronetDataSourceTest { assertThat(e instanceof HttpDataSource.InvalidResponseCodeException).isTrue(); // Check for connection not automatically closed. verify(mockUrlRequest, never()).cancel(); - verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec); + verify(mockTransferListener, never()) + .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true); } } @@ -298,7 +306,8 @@ public final class CronetDataSourceTest { dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE); assertThat(dataSourceUnderTest.open(testPostDataSpec)).isEqualTo(TEST_CONTENT_LENGTH); - verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testPostDataSpec); + verify(mockTransferListener) + .onTransferStart(dataSourceUnderTest, testPostDataSpec, /* isNetwork= */ true); } @Test @@ -327,6 +336,15 @@ public final class CronetDataSourceTest { } } + @Test + public void testHeadRequestOpen() throws HttpDataSourceException { + mockResponseStartSuccess(); + dataSourceUnderTest.open(testHeadDataSpec); + verify(mockTransferListener) + .onTransferStart(dataSourceUnderTest, testHeadDataSpec, /* isNetwork= */ true); + dataSourceUnderTest.close(); + } + @Test public void testRequestReadTwice() throws HttpDataSourceException { mockResponseStartSuccess(); @@ -346,7 +364,8 @@ public final class CronetDataSourceTest { // Should have only called read on cronet once. verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class)); - verify(mockTransferListener, times(2)).onBytesTransferred(dataSourceUnderTest, 8); + verify(mockTransferListener, times(2)) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8); } @Test @@ -386,7 +405,8 @@ public final class CronetDataSourceTest { int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8); assertThat(bytesRead).isEqualTo(8); assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16)); - verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8); + verify(mockTransferListener) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8); } @Test @@ -402,7 +422,8 @@ public final class CronetDataSourceTest { int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16); assertThat(bytesRead).isEqualTo(16); assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16)); - verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16); + verify(mockTransferListener) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16); } @Test @@ -418,7 +439,8 @@ public final class CronetDataSourceTest { int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16); assertThat(bytesRead).isEqualTo(16); assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16)); - verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16); + verify(mockTransferListener) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16); } @Test @@ -433,7 +455,8 @@ public final class CronetDataSourceTest { int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8); assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16)); assertThat(bytesRead).isEqualTo(8); - verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8); + verify(mockTransferListener) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8); } @Test @@ -447,7 +470,8 @@ public final class CronetDataSourceTest { int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 24); assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(0, 16), 24)); assertThat(bytesRead).isEqualTo(16); - verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16); + verify(mockTransferListener) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16); } @Test @@ -464,7 +488,8 @@ public final class CronetDataSourceTest { assertThat(bytesRead).isEqualTo(8); dataSourceUnderTest.close(); - verify(mockTransferListener).onTransferEnd(dataSourceUnderTest); + verify(mockTransferListener) + .onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true); try { bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8); @@ -505,9 +530,12 @@ public final class CronetDataSourceTest { // Should have only called read on cronet once. verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class)); - verify(mockTransferListener, times(1)).onBytesTransferred(dataSourceUnderTest, 8); - verify(mockTransferListener, times(1)).onBytesTransferred(dataSourceUnderTest, 6); - verify(mockTransferListener, times(1)).onBytesTransferred(dataSourceUnderTest, 2); + verify(mockTransferListener, times(1)) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8); + verify(mockTransferListener, times(1)) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6); + verify(mockTransferListener, times(1)) + .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 2); // Now we already returned the 16 bytes initially asked. // Try to read again even though all requested 16 bytes are already returned. @@ -518,7 +546,8 @@ public final class CronetDataSourceTest { assertThat(returnedBuffer).isEqualTo(new byte[16]); // C.RESULT_END_OF_INPUT should not be reported though the TransferListener. verify(mockTransferListener, never()) - .onBytesTransferred(dataSourceUnderTest, C.RESULT_END_OF_INPUT); + .onBytesTransferred( + dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, C.RESULT_END_OF_INPUT); // There should still be only one call to read on cronet. verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class)); // Check for connection not automatically closed. @@ -559,7 +588,8 @@ public final class CronetDataSourceTest { ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10); timedOutLatch.await(); - verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec); + verify(mockTransferListener, never()) + .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true); } @Test @@ -597,11 +627,12 @@ public final class CronetDataSourceTest { thread.interrupt(); timedOutLatch.await(); - verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec); + verify(mockTransferListener, never()) + .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true); } @Test - public void testConnectResponseBeforeTimeout() throws InterruptedException { + public void testConnectResponseBeforeTimeout() throws Exception { long startTimeMs = SystemClock.elapsedRealtime(); final ConditionVariable startCondition = buildUrlRequestStartedCondition(); final CountDownLatch openLatch = new CountDownLatch(1); @@ -625,12 +656,12 @@ public final class CronetDataSourceTest { ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); assertNotCountedDown(openLatch); // The response arrives just in time. - dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo); + dataSourceUnderTest.urlRequestCallback.onResponseStarted(mockUrlRequest, testUrlResponseInfo); openLatch.await(); } @Test - public void testRedirectIncreasesConnectionTimeout() throws InterruptedException { + public void testRedirectIncreasesConnectionTimeout() throws Exception { long startTimeMs = SystemClock.elapsedRealtime(); final ConditionVariable startCondition = buildUrlRequestStartedCondition(); final CountDownLatch timedOutLatch = new CountDownLatch(1); @@ -659,7 +690,7 @@ public final class CronetDataSourceTest { ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); assertNotCountedDown(timedOutLatch); // A redirect arrives just in time. - dataSourceUnderTest.onRedirectReceived( + dataSourceUnderTest.urlRequestCallback.onRedirectReceived( mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl1"); long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1; @@ -667,7 +698,7 @@ public final class CronetDataSourceTest { // We should still be trying to open as we approach the new timeout. assertNotCountedDown(timedOutLatch); // A redirect arrives just in time. - dataSourceUnderTest.onRedirectReceived( + dataSourceUnderTest.urlRequestCallback.onRedirectReceived( mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl2"); newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2; @@ -678,7 +709,8 @@ public final class CronetDataSourceTest { ShadowSystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs + 10); timedOutLatch.await(); - verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec); + verify(mockTransferListener, never()) + .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true); assertThat(openExceptions.get()).isEqualTo(1); } @@ -700,18 +732,17 @@ public final class CronetDataSourceTest { testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders() throws HttpDataSourceException { dataSourceUnderTest = - spy( - new CronetDataSource( - mockCronetEngine, - mockExecutor, - mockContentTypePredicate, - mockTransferListener, - TEST_CONNECT_TIMEOUT_MS, - TEST_READ_TIMEOUT_MS, - true, // resetTimeoutOnRedirects - Clock.DEFAULT, - null, - true)); + new CronetDataSource( + mockCronetEngine, + mockExecutor, + mockContentTypePredicate, + TEST_CONNECT_TIMEOUT_MS, + TEST_READ_TIMEOUT_MS, + true, // resetTimeoutOnRedirects + Clock.DEFAULT, + null, + true); + dataSourceUnderTest.addTransferListener(mockTransferListener); dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE); mockSingleRedirectSuccess(); @@ -732,18 +763,17 @@ public final class CronetDataSourceTest { throws HttpDataSourceException { testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); dataSourceUnderTest = - spy( - new CronetDataSource( - mockCronetEngine, - mockExecutor, - mockContentTypePredicate, - mockTransferListener, - TEST_CONNECT_TIMEOUT_MS, - TEST_READ_TIMEOUT_MS, - true, // resetTimeoutOnRedirects - Clock.DEFAULT, - null, - true)); + new CronetDataSource( + mockCronetEngine, + mockExecutor, + mockContentTypePredicate, + TEST_CONNECT_TIMEOUT_MS, + TEST_READ_TIMEOUT_MS, + true, // resetTimeoutOnRedirects + Clock.DEFAULT, + null, + true); + dataSourceUnderTest.addTransferListener(mockTransferListener); dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE); mockSingleRedirectSuccess(); @@ -772,18 +802,17 @@ public final class CronetDataSourceTest { public void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie() throws HttpDataSourceException { dataSourceUnderTest = - spy( - new CronetDataSource( - mockCronetEngine, - mockExecutor, - mockContentTypePredicate, - mockTransferListener, - TEST_CONNECT_TIMEOUT_MS, - TEST_READ_TIMEOUT_MS, - true, // resetTimeoutOnRedirects - Clock.DEFAULT, - null, - true)); + new CronetDataSource( + mockCronetEngine, + mockExecutor, + mockContentTypePredicate, + TEST_CONNECT_TIMEOUT_MS, + TEST_READ_TIMEOUT_MS, + true, // resetTimeoutOnRedirects + Clock.DEFAULT, + null, + true); + dataSourceUnderTest.addTransferListener(mockTransferListener); mockSingleRedirectSuccess(); mockFollowRedirectSuccess(); @@ -800,7 +829,7 @@ public final class CronetDataSourceTest { // the subsequent open() call succeeds. doThrow(new NullPointerException()) .when(mockTransferListener) - .onTransferEnd(dataSourceUnderTest); + .onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true); dataSourceUnderTest.open(testDataSpec); try { dataSourceUnderTest.close(); @@ -889,7 +918,8 @@ public final class CronetDataSourceTest { new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo); + dataSourceUnderTest.urlRequestCallback.onResponseStarted( + mockUrlRequest, testUrlResponseInfo); return null; } }) @@ -902,7 +932,7 @@ public final class CronetDataSourceTest { new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.onRedirectReceived( + dataSourceUnderTest.urlRequestCallback.onRedirectReceived( mockUrlRequest, createUrlResponseInfo(307), // statusCode "http://redirect.location.com"); @@ -920,12 +950,13 @@ public final class CronetDataSourceTest { public Object answer(InvocationOnMock invocation) throws Throwable { if (!redirectCalled) { redirectCalled = true; - dataSourceUnderTest.onRedirectReceived( + dataSourceUnderTest.urlRequestCallback.onRedirectReceived( mockUrlRequest, createUrlResponseInfoWithUrl("http://example.com/video", 300), "http://example.com/video/redirect"); } else { - dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo); + dataSourceUnderTest.urlRequestCallback.onResponseStarted( + mockUrlRequest, testUrlResponseInfo); } return null; } @@ -939,7 +970,8 @@ public final class CronetDataSourceTest { new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo); + dataSourceUnderTest.urlRequestCallback.onResponseStarted( + mockUrlRequest, testUrlResponseInfo); return null; } }) @@ -952,7 +984,7 @@ public final class CronetDataSourceTest { new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.onFailed( + dataSourceUnderTest.urlRequestCallback.onFailed( mockUrlRequest, createUrlResponseInfo(500), // statusCode mockNetworkException); @@ -970,14 +1002,15 @@ public final class CronetDataSourceTest { @Override public Void answer(InvocationOnMock invocation) throws Throwable { if (positionAndRemaining[1] == 0) { - dataSourceUnderTest.onSucceeded(mockUrlRequest, testUrlResponseInfo); + dataSourceUnderTest.urlRequestCallback.onSucceeded( + mockUrlRequest, testUrlResponseInfo); } else { ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0]; int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining()); inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength)); positionAndRemaining[0] += readLength; positionAndRemaining[1] -= readLength; - dataSourceUnderTest.onReadCompleted( + dataSourceUnderTest.urlRequestCallback.onReadCompleted( mockUrlRequest, testUrlResponseInfo, inputBuffer); } return null; @@ -992,7 +1025,7 @@ public final class CronetDataSourceTest { new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.onFailed( + dataSourceUnderTest.urlRequestCallback.onFailed( mockUrlRequest, createUrlResponseInfo(500), // statusCode mockNetworkException); diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index fa7ac6b9fa..d5a37db013 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -70,7 +70,8 @@ COMMON_OPTIONS="\ --enable-decoder=flac \ " && \ cd "${FFMPEG_EXT_PATH}/jni" && \ -git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \ +(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \ +cd ffmpeg && \ ./configure \ --libdir=android-libs/armeabi-v7a \ --arch=arm \ diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index e2d3a08e36..1630b6f775 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion @@ -32,6 +37,8 @@ android { dependencies { implementation project(modulePrefix + 'library-core') + implementation 'com.android.support:support-annotations:' + supportLibraryVersion + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion } ext { diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index d7687e42ac..13e3964c71 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.ffmpeg; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; @@ -26,29 +27,27 @@ import com.google.android.exoplayer2.audio.DefaultAudioSink; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; +import java.util.Collections; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Decodes and renders audio using FFmpeg. */ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { - /** - * The number of input and output buffers. - */ + /** The number of input and output buffers. */ private static final int NUM_BUFFERS = 16; - /** - * The initial input buffer size. Input buffers are reallocated dynamically if this value is - * insufficient. - */ - private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6; + /** The default input buffer size. */ + private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6; private final boolean enableFloatOutput; - private FfmpegDecoder decoder; + private @MonotonicNonNull FfmpegDecoder decoder; public FfmpegAudioRenderer() { - this(null, null); + this(/* eventHandler= */ null, /* eventListener= */ null); } /** @@ -57,9 +56,15 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ - public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, + public FfmpegAudioRenderer( + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) { - this(eventHandler, eventListener, new DefaultAudioSink(null, audioProcessors), false); + this( + eventHandler, + eventListener, + new DefaultAudioSink(/* audioCapabilities= */ null, audioProcessors), + /* enableFloatOutput= */ false); } /** @@ -72,8 +77,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { * 32-bit float output, any audio processing will be disabled, including playback speed/pitch * adjustment. */ - public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioSink audioSink, boolean enableFloatOutput) { + public FfmpegAudioRenderer( + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + AudioSink audioSink, + boolean enableFloatOutput) { super( eventHandler, eventListener, @@ -86,10 +94,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { @Override protected int supportsFormatInternal(DrmSessionManager drmSessionManager, Format format) { - String sampleMimeType = format.sampleMimeType; - if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(sampleMimeType)) { + Assertions.checkNotNull(format.sampleMimeType); + if (!FfmpegLibrary.isAvailable()) { return FORMAT_UNSUPPORTED_TYPE; - } else if (!FfmpegLibrary.supportsFormat(sampleMimeType) || !isOutputSupported(format)) { + } else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType, format.pcmEncoding) + || !isOutputSupported(format)) { return FORMAT_UNSUPPORTED_SUBTYPE; } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { return FORMAT_UNSUPPORTED_DRM; @@ -106,18 +115,33 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { @Override protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) throws FfmpegDecoderException { - decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, - format.sampleMimeType, format.initializationData, shouldUseFloatOutput(format)); + int initialInputBufferSize = + format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; + decoder = + new FfmpegDecoder( + NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, format, shouldUseFloatOutput(format)); return decoder; } @Override public Format getOutputFormat() { + Assertions.checkNotNull(decoder); int channelCount = decoder.getChannelCount(); int sampleRate = decoder.getSampleRate(); @C.PcmEncoding int encoding = decoder.getEncoding(); - return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE, - Format.NO_VALUE, channelCount, sampleRate, encoding, null, null, 0, null); + return Format.createAudioSampleFormat( + /* id= */ null, + MimeTypes.AUDIO_RAW, + /* codecs= */ null, + Format.NO_VALUE, + Format.NO_VALUE, + channelCount, + sampleRate, + encoding, + Collections.emptyList(), + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); } private boolean isOutputSupported(Format inputFormat) { @@ -125,6 +149,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { } private boolean shouldUseFloatOutput(Format inputFormat) { + Assertions.checkNotNull(inputFormat.sampleMimeType); if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) { return false; } diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java index 91bd82ab2a..6f3c623f3f 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java @@ -15,10 +15,13 @@ */ package com.google.android.exoplayer2.ext.ffmpeg; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.nio.ByteBuffer; @@ -30,13 +33,12 @@ import java.util.List; /* package */ final class FfmpegDecoder extends SimpleDecoder { - // Space for 64 ms of 48 kHz 8 channel 16-bit PCM audio. - private static final int OUTPUT_BUFFER_SIZE_16BIT = 64 * 48 * 8 * 2; - // Space for 64 ms of 48 KhZ 8 channel 32-bit PCM audio. + // Output buffer sizes when decoding PCM mu-law streams, which is the maximum FFmpeg outputs. + private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536; private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2; private final String codecName; - private final byte[] extraData; + private final @Nullable byte[] extraData; private final @C.Encoding int encoding; private final int outputBufferSize; @@ -45,18 +47,26 @@ import java.util.List; private volatile int channelCount; private volatile int sampleRate; - public FfmpegDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, - String mimeType, List initializationData, boolean outputFloat) + public FfmpegDecoder( + int numInputBuffers, + int numOutputBuffers, + int initialInputBufferSize, + Format format, + boolean outputFloat) throws FfmpegDecoderException { super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]); if (!FfmpegLibrary.isAvailable()) { throw new FfmpegDecoderException("Failed to load decoder native libraries."); } - codecName = FfmpegLibrary.getCodecName(mimeType); - extraData = getExtraData(mimeType, initializationData); + Assertions.checkNotNull(format.sampleMimeType); + codecName = + Assertions.checkNotNull( + FfmpegLibrary.getCodecName(format.sampleMimeType, format.pcmEncoding)); + extraData = getExtraData(format.sampleMimeType, format.initializationData); encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT; outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT; - nativeContext = ffmpegInitialize(codecName, extraData, outputFloat); + nativeContext = + ffmpegInitialize(codecName, extraData, outputFloat, format.sampleRate, format.channelCount); if (nativeContext == 0) { throw new FfmpegDecoderException("Initialization failed."); } @@ -84,7 +94,7 @@ import java.util.List; } @Override - protected FfmpegDecoderException decode( + protected @Nullable FfmpegDecoderException decode( DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { if (reset) { nativeContext = ffmpegReset(nativeContext, extraData); @@ -103,6 +113,7 @@ import java.util.List; channelCount = ffmpegGetChannelCount(nativeContext); sampleRate = ffmpegGetSampleRate(nativeContext); if (sampleRate == 0 && "alac".equals(codecName)) { + Assertions.checkNotNull(extraData); // ALAC decoder did not set the sample rate in earlier versions of FFMPEG. // See https://trac.ffmpeg.org/ticket/6096 ParsableByteArray parsableExtraData = new ParsableByteArray(extraData); @@ -148,7 +159,7 @@ import java.util.List; * Returns FFmpeg-compatible codec-specific initialization data ("extra data"), or {@code null} if * not required. */ - private static byte[] getExtraData(String mimeType, List initializationData) { + private static @Nullable byte[] getExtraData(String mimeType, List initializationData) { switch (mimeType) { case MimeTypes.AUDIO_AAC: case MimeTypes.AUDIO_ALAC: @@ -173,12 +184,20 @@ import java.util.List; } } - private native long ffmpegInitialize(String codecName, byte[] extraData, boolean outputFloat); + private native long ffmpegInitialize( + String codecName, + @Nullable byte[] extraData, + boolean outputFloat, + int rawSampleRate, + int rawChannelCount); + private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize, ByteBuffer outputData, int outputSize); private native int ffmpegGetChannelCount(long context); private native int ffmpegGetSampleRate(long context); - private native long ffmpegReset(long context, byte[] extraData); + + private native long ffmpegReset(long context, @Nullable byte[] extraData); + private native void ffmpegRelease(long context); } diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java index 9b3bbbb6ab..e5018a49b3 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.ext.ffmpeg; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.util.LibraryLoader; import com.google.android.exoplayer2.util.MimeTypes; @@ -51,10 +53,8 @@ public final class FfmpegLibrary { return LOADER.isAvailable(); } - /** - * Returns the version of the underlying library if available, or null otherwise. - */ - public static String getVersion() { + /** Returns the version of the underlying library if available, or null otherwise. */ + public static @Nullable String getVersion() { return isAvailable() ? ffmpegGetVersion() : null; } @@ -62,19 +62,21 @@ public final class FfmpegLibrary { * Returns whether the underlying library supports the specified MIME type. * * @param mimeType The MIME type to check. + * @param encoding The PCM encoding for raw audio. */ - public static boolean supportsFormat(String mimeType) { + public static boolean supportsFormat(String mimeType, @C.PcmEncoding int encoding) { if (!isAvailable()) { return false; } - String codecName = getCodecName(mimeType); + String codecName = getCodecName(mimeType, encoding); return codecName != null && ffmpegHasDecoder(codecName); } /** - * Returns the name of the FFmpeg decoder that could be used to decode {@code mimeType}. + * Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null} + * if it's unsupported. */ - /* package */ static String getCodecName(String mimeType) { + /* package */ static @Nullable String getCodecName(String mimeType, @C.PcmEncoding int encoding) { switch (mimeType) { case MimeTypes.AUDIO_AAC: return "aac"; @@ -85,6 +87,7 @@ public final class FfmpegLibrary { case MimeTypes.AUDIO_AC3: return "ac3"; case MimeTypes.AUDIO_E_AC3: + case MimeTypes.AUDIO_E_AC3_JOC: return "eac3"; case MimeTypes.AUDIO_TRUEHD: return "truehd"; @@ -103,6 +106,14 @@ public final class FfmpegLibrary { return "flac"; case MimeTypes.AUDIO_ALAC: return "alac"; + case MimeTypes.AUDIO_RAW: + if (encoding == C.ENCODING_PCM_MU_LAW) { + return "pcm_mulaw"; + } else if (encoding == C.ENCODING_PCM_A_LAW) { + return "pcm_alaw"; + } else { + return null; + } default: return null; } diff --git a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc index d077c819ab..87579ebb9a 100644 --- a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc +++ b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc @@ -27,6 +27,7 @@ extern "C" { #endif #include #include +#include #include #include } @@ -72,8 +73,9 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName); * provided extraData as initialization data for the decoder if it is non-NULL. * Returns the created context. */ -AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, - jbyteArray extraData, jboolean outputFloat); +AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData, + jboolean outputFloat, jint rawSampleRate, + jint rawChannelCount); /** * Decodes the packet into the output buffer, returning the number of bytes @@ -110,13 +112,14 @@ LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) { } DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData, - jboolean outputFloat) { + jboolean outputFloat, jint rawSampleRate, jint rawChannelCount) { AVCodec *codec = getCodecByName(env, codecName); if (!codec) { LOGE("Codec not found."); return 0L; } - return (jlong) createContext(env, codec, extraData, outputFloat); + return (jlong)createContext(env, codec, extraData, outputFloat, rawSampleRate, + rawChannelCount); } DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData, @@ -180,8 +183,11 @@ DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) { LOGE("Unexpected error finding codec %d.", codecId); return 0L; } - return (jlong) createContext(env, codec, extraData, - context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT); + jboolean outputFloat = + (jboolean)(context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT); + return (jlong)createContext(env, codec, extraData, outputFloat, + /* rawSampleRate= */ -1, + /* rawChannelCount= */ -1); } avcodec_flush_buffers(context); @@ -204,8 +210,9 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName) { return codec; } -AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, - jbyteArray extraData, jboolean outputFloat) { +AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData, + jboolean outputFloat, jint rawSampleRate, + jint rawChannelCount) { AVCodecContext *context = avcodec_alloc_context3(codec); if (!context) { LOGE("Failed to allocate context."); @@ -225,6 +232,12 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, } env->GetByteArrayRegion(extraData, 0, size, (jbyte *) context->extradata); } + if (context->codec_id == AV_CODEC_ID_PCM_MULAW || + context->codec_id == AV_CODEC_ID_PCM_ALAW) { + context->sample_rate = rawSampleRate; + context->channels = rawChannelCount; + context->channel_layout = av_get_default_channel_layout(rawChannelCount); + } int result = avcodec_open2(context, codec, NULL); if (result < 0) { logError("avcodec_open2", result); diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 609953130b..98b81d911a 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java index 8124f1958a..f8e61a0609 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java @@ -67,6 +67,6 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase { decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); seeker.setSeekTargetUs(/* timeUs= */ 1000); - assertThat(seeker.hasPendingSeek()).isTrue(); + assertThat(seeker.isSeeking()).isTrue(); } } diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java index b236b706b8..07b7a0ccdb 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java @@ -64,8 +64,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase { } } - private static class TestPlaybackRunnable extends Player.DefaultEventListener - implements Runnable { + private static class TestPlaybackRunnable implements Player.EventListener, Runnable { private final Context context; private final Uri uri; diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java index 0bbee1ea30..b9c6ea06dd 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java @@ -15,15 +15,11 @@ */ package com.google.android.exoplayer2.ext.flac; -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.BinarySearchSeeker; import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.FlacStreamInfo; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.nio.ByteBuffer; @@ -33,111 +29,51 @@ import java.nio.ByteBuffer; *

This seeker performs seeking by using binary search within the stream, until it finds the * frame that contains the target sample. */ -/* package */ final class FlacBinarySearchSeeker { +/* package */ final class FlacBinarySearchSeeker extends BinarySearchSeeker { - /** - * When seeking within the source, if the offset is smaller than or equal to this value, the seek - * operation will be performed using a skip operation. Otherwise, the source will be reloaded at - * the new seek position. - */ - private static final long MAX_SKIP_BYTES = 256 * 1024; - - private final FlacStreamInfo streamInfo; - private final FlacBinarySearchSeekMap seekMap; private final FlacDecoderJni decoderJni; - private final long firstFramePosition; - private final long inputLength; - private final long approxBytesPerFrame; - - private @Nullable SeekOperationParams pendingSeekOperationParams; - public FlacBinarySearchSeeker( FlacStreamInfo streamInfo, long firstFramePosition, long inputLength, FlacDecoderJni decoderJni) { - this.streamInfo = Assertions.checkNotNull(streamInfo); + super( + new FlacSeekTimestampConverter(streamInfo), + new FlacTimestampSeeker(decoderJni), + streamInfo.durationUs(), + /* floorTimePosition= */ 0, + /* ceilingTimePosition= */ streamInfo.totalSamples, + /* floorBytePosition= */ firstFramePosition, + /* ceilingBytePosition= */ inputLength, + /* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(), + /* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize)); this.decoderJni = Assertions.checkNotNull(decoderJni); - this.firstFramePosition = firstFramePosition; - this.inputLength = inputLength; - this.approxBytesPerFrame = streamInfo.getApproxBytesPerFrame(); - - pendingSeekOperationParams = null; - seekMap = - new FlacBinarySearchSeekMap( - streamInfo, - firstFramePosition, - inputLength, - streamInfo.durationUs(), - approxBytesPerFrame); } - /** Returns the seek map for the wrapped FLAC stream. */ - public SeekMap getSeekMap() { - return seekMap; + @Override + protected void onSeekOperationFinished(boolean foundTargetFrame, long resultPosition) { + if (!foundTargetFrame) { + // If we can't find the target frame (sample), we need to reset the decoder jni so that + // it can continue from the result position. + decoderJni.reset(resultPosition); + } } - /** Sets the target time in microseconds within the stream to seek to. */ - public void setSeekTargetUs(long timeUs) { - if (pendingSeekOperationParams != null && pendingSeekOperationParams.seekTimeUs == timeUs) { - return; + private static final class FlacTimestampSeeker implements TimestampSeeker { + + private final FlacDecoderJni decoderJni; + + private FlacTimestampSeeker(FlacDecoderJni decoderJni) { + this.decoderJni = decoderJni; } - pendingSeekOperationParams = - new SeekOperationParams( - timeUs, - streamInfo.getSampleIndex(timeUs), - /* floorSample= */ 0, - /* ceilingSample= */ streamInfo.totalSamples, - /* floorPosition= */ firstFramePosition, - /* ceilingPosition= */ inputLength, - approxBytesPerFrame); - } - - /** Returns whether the last operation set by {@link #setSeekTargetUs(long)} is still pending. */ - public boolean hasPendingSeek() { - return pendingSeekOperationParams != null; - } - - /** - * Continues to handle the pending seek operation. Returns one of the {@code RESULT_} values from - * {@link Extractor}. - * - * @param input The {@link ExtractorInput} from which data should be read. - * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated - * to hold the position of the required seek. - * @param outputBuffer If {@link Extractor#RESULT_CONTINUE} is returned, this byte buffer maybe - * updated to hold the extracted frame that contains the target sample. The caller needs to - * check the byte buffer limit to see if an extracted frame is available. - * @return One of the {@code RESULT_} values defined in {@link Extractor}. - * @throws IOException If an error occurred reading from the input. - * @throws InterruptedException If the thread was interrupted. - */ - public int handlePendingSeek( - ExtractorInput input, PositionHolder seekPositionHolder, ByteBuffer outputBuffer) - throws InterruptedException, IOException { - outputBuffer.position(0); - outputBuffer.limit(0); - while (true) { - long floorPosition = pendingSeekOperationParams.floorPosition; - long ceilingPosition = pendingSeekOperationParams.ceilingPosition; - long searchPosition = pendingSeekOperationParams.nextSearchPosition; - - // streamInfo may not contain minFrameSize, in which case this value will be 0. - int minFrameSize = Math.max(1, streamInfo.minFrameSize); - if (floorPosition + minFrameSize >= ceilingPosition) { - // The seeking range is too small for more than 1 frame, so we can just continue from - // the floor position. - pendingSeekOperationParams = null; - decoderJni.reset(floorPosition); - return seekToPosition(input, floorPosition, seekPositionHolder); - } - - if (!skipInputUntilPosition(input, searchPosition)) { - return seekToPosition(input, searchPosition, seekPositionHolder); - } - + @Override + public TimestampSearchResult searchForTimestamp( + ExtractorInput input, long targetSampleIndex, OutputFrameHolder outputFrameHolder) + throws IOException, InterruptedException { + ByteBuffer outputBuffer = outputFrameHolder.byteBuffer; + long searchPosition = input.getPosition(); decoderJni.reset(searchPosition); try { decoderJni.decodeSampleWithBacktrackPosition( @@ -145,11 +81,10 @@ import java.nio.ByteBuffer; } catch (FlacDecoderJni.FlacFrameDecodeException e) { // For some reasons, the extractor can't find a frame mid-stream. // Stop the seeking and let it re-try playing at the last search position. - pendingSeekOperationParams = null; - throw new IOException("Cannot read frame at position " + searchPosition, e); + return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT; } if (outputBuffer.limit() == 0) { - return Extractor.RESULT_END_OF_INPUT; + return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT; } long lastFrameSampleIndex = decoderJni.getLastFrameFirstSampleIndex(); @@ -157,184 +92,35 @@ import java.nio.ByteBuffer; long nextFrameSamplePosition = decoderJni.getDecodePosition(); boolean targetSampleInLastFrame = - lastFrameSampleIndex <= pendingSeekOperationParams.targetSample - && nextFrameSampleIndex > pendingSeekOperationParams.targetSample; + lastFrameSampleIndex <= targetSampleIndex && nextFrameSampleIndex > targetSampleIndex; if (targetSampleInLastFrame) { - pendingSeekOperationParams = null; - return Extractor.RESULT_CONTINUE; - } - - if (nextFrameSampleIndex <= pendingSeekOperationParams.targetSample) { - pendingSeekOperationParams.updateSeekFloor(nextFrameSampleIndex, nextFrameSamplePosition); + // We are holding the target frame in outputFrameHolder. Set its presentation time now. + outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp(); + return TimestampSearchResult.targetFoundResult(input.getPosition()); + } else if (nextFrameSampleIndex <= targetSampleIndex) { + return TimestampSearchResult.underestimatedResult( + nextFrameSampleIndex, nextFrameSamplePosition); } else { - pendingSeekOperationParams.updateSeekCeiling(lastFrameSampleIndex, searchPosition); + return TimestampSearchResult.overestimatedResult(lastFrameSampleIndex, searchPosition); } } } - private boolean skipInputUntilPosition(ExtractorInput input, long position) - throws IOException, InterruptedException { - long bytesToSkip = position - input.getPosition(); - if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) { - input.skipFully((int) bytesToSkip); - return true; - } - return false; - } - - private int seekToPosition( - ExtractorInput input, long position, PositionHolder seekPositionHolder) { - if (position == input.getPosition()) { - return Extractor.RESULT_CONTINUE; - } else { - seekPositionHolder.position = position; - return Extractor.RESULT_SEEK; - } - } - /** - * Contains parameters for a pending seek operation by {@link FlacBinarySearchSeeker}. - * - *

This class holds parameters for a binary-search for the {@code targetSample} in the range - * [floorPosition, ceilingPosition). + * A {@link SeekTimestampConverter} implementation that returns the frame index (sample index) as + * the timestamp for a stream seek time position. */ - private static final class SeekOperationParams { - private final long seekTimeUs; - private final long targetSample; - private final long approxBytesPerFrame; - private long floorSample; - private long ceilingSample; - private long floorPosition; - private long ceilingPosition; - private long nextSearchPosition; - - private SeekOperationParams( - long seekTimeUs, - long targetSample, - long floorSample, - long ceilingSample, - long floorPosition, - long ceilingPosition, - long approxBytesPerFrame) { - this.seekTimeUs = seekTimeUs; - this.floorSample = floorSample; - this.ceilingSample = ceilingSample; - this.floorPosition = floorPosition; - this.ceilingPosition = ceilingPosition; - this.targetSample = targetSample; - this.approxBytesPerFrame = approxBytesPerFrame; - updateNextSearchPosition(); - } - - /** Updates the floor constraints (inclusive) of the seek operation. */ - private void updateSeekFloor(long floorSample, long floorPosition) { - this.floorSample = floorSample; - this.floorPosition = floorPosition; - updateNextSearchPosition(); - } - - /** Updates the ceiling constraints (exclusive) of the seek operation. */ - private void updateSeekCeiling(long ceilingSample, long ceilingPosition) { - this.ceilingSample = ceilingSample; - this.ceilingPosition = ceilingPosition; - updateNextSearchPosition(); - } - - private void updateNextSearchPosition() { - this.nextSearchPosition = - getNextSearchPosition( - targetSample, - floorSample, - ceilingSample, - floorPosition, - ceilingPosition, - approxBytesPerFrame); - } - - /** - * Returns the next position in FLAC stream to search for target sample, given [floorPosition, - * ceilingPosition). - */ - private static long getNextSearchPosition( - long targetSample, - long floorSample, - long ceilingSample, - long floorPosition, - long ceilingPosition, - long approxBytesPerFrame) { - if (floorPosition + 1 >= ceilingPosition || floorSample + 1 >= ceilingSample) { - return floorPosition; - } - long samplesToSkip = targetSample - floorSample; - long estimatedBytesPerSample = - Math.max(1, (ceilingPosition - floorPosition) / (ceilingSample - floorSample)); - // In the stream, the samples are accessed in a group of frame. Given a stream position, the - // seeker will be able to find the first frame following that position. - // Hence, if our target sample is in the middle of a frame, and our estimate position is - // correct, or very near the actual sample position, the seeker will keep accessing the next - // frame, rather than the frame that contains the target sample. - // Moreover, it's better to under-estimate rather than over-estimate, because the extractor - // input can skip forward easily, but cannot rewind easily (it may require a new connection - // to be made). - // Therefore, we should reduce the estimated position by some amount, so it will converge to - // the correct frame earlier. - long bytesToSkip = samplesToSkip * estimatedBytesPerSample; - long confidenceInterval = bytesToSkip / 20; - - long estimatedFramePosition = floorPosition + bytesToSkip - (approxBytesPerFrame - 1); - long estimatedPosition = estimatedFramePosition - confidenceInterval; - - return Util.constrainValue(estimatedPosition, floorPosition, ceilingPosition - 1); - } - } - - /** - * A {@link SeekMap} implementation that returns the estimated byte location from {@link - * SeekOperationParams#getNextSearchPosition(long, long, long, long, long, long)} for each {@link - * #getSeekPoints(long)} query. - */ - private static final class FlacBinarySearchSeekMap implements SeekMap { + private static final class FlacSeekTimestampConverter implements SeekTimestampConverter { private final FlacStreamInfo streamInfo; - private final long firstFramePosition; - private final long inputLength; - private final long approxBytesPerFrame; - private final long durationUs; - private FlacBinarySearchSeekMap( - FlacStreamInfo streamInfo, - long firstFramePosition, - long inputLength, - long durationUs, - long approxBytesPerFrame) { + public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) { this.streamInfo = streamInfo; - this.firstFramePosition = firstFramePosition; - this.inputLength = inputLength; - this.approxBytesPerFrame = approxBytesPerFrame; - this.durationUs = durationUs; } @Override - public boolean isSeekable() { - return true; - } - - @Override - public SeekPoints getSeekPoints(long timeUs) { - long nextSearchPosition = - SeekOperationParams.getNextSearchPosition( - streamInfo.getSampleIndex(timeUs), - /* floorSample= */ 0, - /* ceilingSample= */ streamInfo.totalSamples, - /* floorPosition= */ firstFramePosition, - /* ceilingPosition= */ inputLength, - approxBytesPerFrame); - return new SeekPoints(new SeekPoint(timeUs, nextSearchPosition)); - } - - @Override - public long getDurationUs() { - return durationUs; + public long timeUsToTargetTime(long timeUs) { + return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs); } } } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java index e8a04e06ae..2d74bce5f1 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.flac; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; @@ -37,11 +38,17 @@ import java.util.List; * * @param numInputBuffers The number of input buffers. * @param numOutputBuffers The number of output buffers. + * @param maxInputBufferSize The maximum required input buffer size if known, or {@link + * Format#NO_VALUE} otherwise. * @param initializationData Codec-specific initialization data. It should contain only one entry - * which is the flac file header. + * which is the flac file header. * @throws FlacDecoderException Thrown if an exception occurs when initializing the decoder. */ - public FlacDecoder(int numInputBuffers, int numOutputBuffers, List initializationData) + public FlacDecoder( + int numInputBuffers, + int numOutputBuffers, + int maxInputBufferSize, + List initializationData) throws FlacDecoderException { super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]); if (initializationData.size() != 1) { @@ -60,7 +67,9 @@ import java.util.List; throw new FlacDecoderException("Metadata decoding failed"); } - setInitialInputBufferSize(streamInfo.maxFrameSize); + int initialInputBufferSize = + maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize; + setInitialInputBufferSize(initialInputBufferSize); maxOutputBufferSize = streamInfo.maxDecodedFrameSize(); } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index a5efeb69f9..b6eec765d1 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -21,6 +21,7 @@ import android.support.annotation.IntDef; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.BinarySearchSeeker; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -46,24 +47,14 @@ import java.util.Arrays; */ public final class FlacExtractor implements Extractor { - /** - * Factory that returns one extractor which is a {@link FlacExtractor}. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { - - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new FlacExtractor()}; - } - - }; + /** Factory that returns one extractor which is a {@link FlacExtractor}. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()}; /** Flags controlling the behavior of the extractor. */ @Retention(RetentionPolicy.SOURCE) @IntDef( - flag = true, - value = {FLAG_DISABLE_ID3_METADATA} - ) + flag = true, + value = {FLAG_DISABLE_ID3_METADATA}) public @interface Flags {} /** @@ -88,6 +79,7 @@ public final class FlacExtractor implements Extractor { private ParsableByteArray outputBuffer; private ByteBuffer outputByteBuffer; + private BinarySearchSeeker.OutputFrameHolder outputFrameHolder; private FlacStreamInfo streamInfo; private Metadata id3Metadata; @@ -140,7 +132,7 @@ public final class FlacExtractor implements Extractor { decoderJni.setData(input); readPastStreamInfo(input); - if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.hasPendingSeek()) { + if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) { return handlePendingSeek(input, seekPosition); } @@ -224,6 +216,7 @@ public final class FlacExtractor implements Extractor { outputFormat(streamInfo); outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); outputByteBuffer = ByteBuffer.wrap(outputBuffer.data); + outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer); } private FlacStreamInfo decodeStreamInfo(ExtractorInput input) @@ -286,9 +279,10 @@ public final class FlacExtractor implements Extractor { private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition) throws InterruptedException, IOException { int seekResult = - flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputByteBuffer); + flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder); + ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { - writeLastSampleToOutput(outputByteBuffer.limit(), decoderJni.getLastFrameTimestamp()); + writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs); } return seekResult; } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java index a72b03cd44..fa66abbdc6 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -65,7 +65,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { @Override protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) throws FlacDecoderException { - return new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.initializationData); + return new FlacDecoder( + NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData); } } diff --git a/extensions/flac/src/test/AndroidManifest.xml b/extensions/flac/src/test/AndroidManifest.xml new file mode 100644 index 0000000000..1d68b376ac --- /dev/null +++ b/extensions/flac/src/test/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + diff --git a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java new file mode 100644 index 0000000000..e08f4dc28c --- /dev/null +++ b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.flac; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.amr.AmrExtractor; +import com.google.android.exoplayer2.extractor.flv.FlvExtractor; +import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; +import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; +import com.google.android.exoplayer2.extractor.ogg.OggExtractor; +import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; +import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; +import com.google.android.exoplayer2.extractor.ts.PsExtractor; +import com.google.android.exoplayer2.extractor.ts.TsExtractor; +import com.google.android.exoplayer2.extractor.wav.WavExtractor; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test for {@link DefaultExtractorsFactory}. */ +@RunWith(RobolectricTestRunner.class) +public final class DefaultExtractorsFactoryTest { + + @Test + public void testCreateExtractors_returnExpectedClasses() { + DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory(); + + Extractor[] extractors = defaultExtractorsFactory.createExtractors(); + List listCreatedExtractorClasses = new ArrayList<>(); + for (Extractor extractor : extractors) { + listCreatedExtractorClasses.add(extractor.getClass()); + } + + Class[] expectedExtractorClassses = + new Class[] { + MatroskaExtractor.class, + FragmentedMp4Extractor.class, + Mp4Extractor.class, + Mp3Extractor.class, + AdtsExtractor.class, + Ac3Extractor.class, + TsExtractor.class, + FlvExtractor.class, + OggExtractor.class, + PsExtractor.class, + WavExtractor.class, + AmrExtractor.class, + FlacExtractor.class + }; + + assertThat(listCreatedExtractorClasses).containsNoDuplicates(); + assertThat(listCreatedExtractorClasses).containsExactlyElementsIn(expectedExtractorClassses); + } +} diff --git a/extensions/flac/src/test/resources/robolectric.properties b/extensions/flac/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..2f3210368e --- /dev/null +++ b/extensions/flac/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +manifest=src/test/AndroidManifest.xml diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index 87e72939c5..af973e1345 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion 19 targetSdkVersion project.ext.targetSdkVersion diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java index 1b595d6886..eca31c98e4 100644 --- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java +++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; +import com.google.android.exoplayer2.util.Assertions; import com.google.vr.sdk.audio.GvrAudioSurround; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -148,18 +149,21 @@ public final class GvrAudioProcessor implements AudioProcessor { @Override public void queueInput(ByteBuffer input) { int position = input.position(); + Assertions.checkNotNull(gvrAudioSurround); int readBytes = gvrAudioSurround.addInput(input, position, input.limit() - position); input.position(position + readBytes); } @Override public void queueEndOfStream() { + Assertions.checkNotNull(gvrAudioSurround); inputEnded = true; gvrAudioSurround.triggerProcessing(); } @Override public ByteBuffer getOutput() { + Assertions.checkNotNull(gvrAudioSurround); int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity()); buffer.position(0).limit(writtenBytes); return buffer; @@ -167,6 +171,7 @@ public final class GvrAudioProcessor implements AudioProcessor { @Override public boolean isEnded() { + Assertions.checkNotNull(gvrAudioSurround); return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0; } diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 3529e05380..cf6938a2b1 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion @@ -26,19 +31,25 @@ android { } dependencies { - // This dependency is necessary to force the supportLibraryVersion of - // com.android.support:support-v4 to be used. Else an older version (25.2.0) - // is included via: - // com.google.android.gms:play-services-ads:12.0.0 - // |-- com.google.android.gms:play-services-ads-lite:12.0.0 - // |-- com.google.android.gms:play-services-basement:12.0.0 - // |-- com.android.support:support-v4:26.1.0 - api 'com.android.support:support-v4:' + supportLibraryVersion - api 'com.google.ads.interactivemedia.v3:interactivemedia:3.8.5' + api 'com.google.ads.interactivemedia.v3:interactivemedia:3.9.4' implementation project(modulePrefix + 'library-core') - implementation 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion + implementation 'com.google.android.gms:play-services-ads:15.0.1' + // These dependencies are necessary to force the supportLibraryVersion of + // com.android.support:support-v4 and com.android.support:customtabs to be + // used. Else older versions are used, for example via: + // com.google.android.gms:play-services-ads:15.0.1 + // |-- com.android.support:customtabs:26.1.0 + implementation 'com.android.support:support-v4:' + supportLibraryVersion + implementation 'com.android.support:customtabs:' + supportLibraryVersion + testImplementation 'com.google.truth:truth:' + truthVersion + testImplementation 'junit:junit:' + junitVersion + testImplementation 'org.mockito:mockito-core:' + mockitoVersion + testImplementation 'org.robolectric:robolectric:' + robolectricVersion + testImplementation project(modulePrefix + 'testutils-robolectric') } +apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' + ext { javadocTitle = 'IMA extension' } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 2d9ddfb288..bf1cdfe02c 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -22,7 +22,6 @@ import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.util.Log; import android.view.ViewGroup; -import android.webkit.WebView; import com.google.ads.interactivemedia.v3.api.Ad; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; import com.google.ads.interactivemedia.v3.api.AdError; @@ -38,6 +37,7 @@ import com.google.ads.interactivemedia.v3.api.AdsManager; import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; import com.google.ads.interactivemedia.v3.api.AdsRequest; +import com.google.ads.interactivemedia.v3.api.CompanionAdSlot; import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; @@ -53,6 +53,7 @@ import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; @@ -62,15 +63,20 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; -/** - * Loads ads using the IMA SDK. All methods are called on the main thread. - */ -public final class ImaAdsLoader extends Player.DefaultEventListener implements AdsLoader, - VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener { +/** Loads ads using the IMA SDK. All methods are called on the main thread. */ +public final class ImaAdsLoader + implements Player.EventListener, + AdsLoader, + VideoAdPlayer, + ContentProgressProvider, + AdErrorListener, + AdsLoadedListener, + AdEventListener { static { ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); @@ -85,6 +91,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private @Nullable AdEventListener adEventListener; private int vastLoadTimeoutMs; private int mediaLoadTimeoutMs; + private ImaFactory imaFactory; /** * Creates a new builder for {@link ImaAdsLoader}. @@ -95,6 +102,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A this.context = Assertions.checkNotNull(context); vastLoadTimeoutMs = TIMEOUT_UNSET; mediaLoadTimeoutMs = TIMEOUT_UNSET; + imaFactory = new DefaultImaFactory(); } /** @@ -149,6 +157,12 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return this; } + // @VisibleForTesting + /* package */ Builder setImaFactory(ImaFactory imaFactory) { + this.imaFactory = Assertions.checkNotNull(imaFactory); + return this; + } + /** * Returns a new {@link ImaAdsLoader} for the specified ad tag. * @@ -165,7 +179,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A null, vastLoadTimeoutMs, mediaLoadTimeoutMs, - adEventListener); + adEventListener, + imaFactory); } /** @@ -183,7 +198,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adsResponse, vastLoadTimeoutMs, mediaLoadTimeoutMs, - adEventListener); + adEventListener, + imaFactory); } } @@ -210,14 +226,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A /** The maximum duration before an ad break that IMA may start preloading the next ad. */ private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000; - /** - * The "Skip ad" button rendered in the IMA WebView does not gain focus by default and cannot be - * clicked via a keypress event. Workaround this issue by calling focus() on the HTML element in - * the WebView directly when an ad starts. See [Internal: b/62371030]. - */ - private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:" - + "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}"; - private static final int TIMEOUT_UNSET = -1; /** The state of ad playback. */ @@ -242,9 +250,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private final int vastLoadTimeoutMs; private final int mediaLoadTimeoutMs; private final @Nullable AdEventListener adEventListener; + private final ImaFactory imaFactory; private final Timeline.Period period; private final List adCallbacks; - private final ImaSdkFactory imaSdkFactory; private final AdDisplayContainer adDisplayContainer; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; @@ -252,9 +260,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private List supportedMimeTypes; private EventListener eventListener; private Player player; - private ViewGroup adUiViewGroup; private VideoProgressUpdate lastContentProgress; private VideoProgressUpdate lastAdProgress; + private int lastVolumePercentage; private AdsManager adsManager; private AdLoadException pendingAdLoadError; @@ -267,13 +275,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A /** The expected ad group index that IMA should load next. */ private int expectedAdGroupIndex; - /** - * The index of the current ad group that IMA is loading. - */ + /** The index of the current ad group that IMA is loading. */ private int adGroupIndex; - /** - * Whether IMA has sent an ad event to pause content since the last resume content event. - */ + /** Whether IMA has sent an ad event to pause content since the last resume content event. */ private boolean imaPausedContent; /** The current ad playback state. */ private @ImaAdState int imaAdState; @@ -285,9 +289,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Fields tracking the player/loader state. - /** - * Whether the player is playing an ad. - */ + /** Whether the player is playing an ad. */ private boolean playingAd; /** * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET} @@ -310,13 +312,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * content progress should increase. {@link C#TIME_UNSET} otherwise. */ private long fakeContentProgressOffsetMs; - /** - * Stores the pending content position when a seek operation was intercepted to play an ad. - */ + /** Stores the pending content position when a seek operation was intercepted to play an ad. */ private long pendingContentPositionMs; - /** - * Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. - */ + /** Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */ private boolean sentPendingContentPositionMs; /** @@ -337,7 +335,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A /* adsResponse= */ null, /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, - /* adEventListener= */ null); + /* adEventListener= */ null, + /* imaFactory= */ new DefaultImaFactory()); } /** @@ -360,7 +359,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A /* adsResponse= */ null, /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, - /* adEventListener= */ null); + /* adEventListener= */ null, + /* imaFactory= */ new DefaultImaFactory()); } private ImaAdsLoader( @@ -370,26 +370,30 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Nullable String adsResponse, int vastLoadTimeoutMs, int mediaLoadTimeoutMs, - @Nullable AdEventListener adEventListener) { + @Nullable AdEventListener adEventListener, + ImaFactory imaFactory) { Assertions.checkArgument(adTagUri != null || adsResponse != null); this.adTagUri = adTagUri; this.adsResponse = adsResponse; this.vastLoadTimeoutMs = vastLoadTimeoutMs; this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; this.adEventListener = adEventListener; - period = new Timeline.Period(); - adCallbacks = new ArrayList<>(1); - imaSdkFactory = ImaSdkFactory.getInstance(); - adDisplayContainer = imaSdkFactory.createAdDisplayContainer(); - adDisplayContainer.setPlayer(this); + this.imaFactory = imaFactory; if (imaSdkSettings == null) { - imaSdkSettings = imaSdkFactory.createImaSdkSettings(); + imaSdkSettings = imaFactory.createImaSdkSettings(); + if (DEBUG) { + imaSdkSettings.setDebugMode(true); + } } imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); - adsLoader = imaSdkFactory.createAdsLoader(context, imaSdkSettings); - adsLoader.addAdErrorListener(this); - adsLoader.addAdsLoadedListener(this); + adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings); + period = new Timeline.Period(); + adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); + adDisplayContainer = imaFactory.createAdDisplayContainer(); + adDisplayContainer.setPlayer(/* videoAdPlayer= */ this); + adsLoader.addAdErrorListener(/* adErrorListener= */ this); + adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this); fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; fakeContentProgressOffsetMs = C.TIME_UNSET; pendingContentPositionMs = C.TIME_UNSET; @@ -405,6 +409,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return adsLoader; } + /** + * Sets the slots for displaying companion ads. Individual slots can be created using {@link + * ImaSdkFactory#createCompanionAdSlot()}. + * + * @param companionSlots Slots for displaying companion ads. + * @see AdDisplayContainer#setCompanionSlots(Collection) + */ + public void setCompanionSlots(Collection companionSlots) { + adDisplayContainer.setCompanionSlots(companionSlots); + } + /** * Requests ads, if they have not already been requested. Must be called on the main thread. * @@ -421,7 +436,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } adDisplayContainer.setAdContainer(adUiViewGroup); pendingAdRequestContext = new Object(); - AdsRequest request = imaSdkFactory.createAdsRequest(); + AdsRequest request = imaFactory.createAdsRequest(); if (adTagUri != null) { request.setAdTagUrl(adTagUri.toString()); } else /* adsResponse != null */ { @@ -447,9 +462,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } else if (contentType == C.TYPE_HLS) { supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8); } else if (contentType == C.TYPE_OTHER) { - supportedMimeTypes.addAll(Arrays.asList( - MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_WEBM, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_MPEG, - MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG)); + supportedMimeTypes.addAll( + Arrays.asList( + MimeTypes.VIDEO_MP4, + MimeTypes.VIDEO_WEBM, + MimeTypes.VIDEO_H263, + MimeTypes.AUDIO_MP4, + MimeTypes.AUDIO_MPEG)); } else if (contentType == C.TYPE_SS) { // IMA does not support Smooth Streaming ad media. } @@ -461,7 +480,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) { this.player = player; this.eventListener = eventListener; - this.adUiViewGroup = adUiViewGroup; + lastVolumePercentage = 0; lastAdProgress = null; lastContentProgress = null; adDisplayContainer.setAdContainer(adUiViewGroup); @@ -490,12 +509,12 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A playingAd ? C.msToUs(player.getCurrentPosition()) : 0); adsManager.pause(); } + lastVolumePercentage = getVolume(); lastAdProgress = getAdProgress(); lastContentProgress = getContentProgress(); player.removeListener(this); player = null; eventListener = null; - adUiViewGroup = null; } @Override @@ -505,6 +524,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adsManager.destroy(); adsManager = null; } + imaPausedContent = false; + imaAdState = IMA_AD_STATE_NONE; + pendingAdLoadError = null; + adPlaybackState = AdPlaybackState.NONE; + updateAdPlaybackState(); } @Override @@ -554,7 +578,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.d(TAG, "onAdEvent: " + adEventType); } if (adsManager == null) { - Log.w(TAG, "Dropping ad event after release: " + adEvent); + Log.w(TAG, "Ignoring AdEvent after release: " + adEvent); return; } try { @@ -607,8 +631,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; - expectedAdGroupIndex = + int adGroupIndexForPosition = adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); + if (adGroupIndexForPosition != C.INDEX_UNSET) { + expectedAdGroupIndex = adGroupIndexForPosition; + } } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) { contentPositionMs = player.getCurrentPosition(); // Update the expected ad group index for the current content position. The update is delayed @@ -647,9 +674,37 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } + @Override + public int getVolume() { + if (player == null) { + return lastVolumePercentage; + } + + Player.AudioComponent audioComponent = player.getAudioComponent(); + if (audioComponent != null) { + return (int) (audioComponent.getVolume() * 100); + } + + // Check for a selected track using an audio renderer. + TrackSelectionArray trackSelections = player.getCurrentTrackSelections(); + for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) { + if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) { + return 100; + } + } + return 0; + } + @Override public void loadAd(String adUriString) { try { + if (DEBUG) { + Log.d(TAG, "loadAd in ad group " + adGroupIndex); + } + if (adsManager == null) { + Log.w(TAG, "Ignoring loadAd after release"); + return; + } if (adGroupIndex == C.INDEX_UNSET) { Log.w( TAG, @@ -658,9 +713,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adGroupIndex = expectedAdGroupIndex; adsManager.start(); } - if (DEBUG) { - Log.d(TAG, "loadAd in ad group " + adGroupIndex); - } int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex); if (adIndexInAdGroup == C.INDEX_UNSET) { Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads"); @@ -689,6 +741,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (DEBUG) { Log.d(TAG, "playAd"); } + if (adsManager == null) { + Log.w(TAG, "Ignoring playAd after release"); + return; + } switch (imaAdState) { case IMA_AD_STATE_PLAYING: // IMA does not always call stopAd before resuming content. @@ -732,6 +788,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (DEBUG) { Log.d(TAG, "stopAd"); } + if (adsManager == null) { + Log.w(TAG, "Ignoring stopAd after release"); + return; + } if (player == null) { // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. Log.w(TAG, "Unexpected stopAd while detached"); @@ -771,8 +831,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Player.EventListener implementation. @Override - public void onTimelineChanged(Timeline timeline, Object manifest, - @Player.TimelineChangeReason int reason) { + public void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { if (reason == Player.TIMELINE_CHANGE_REASON_RESET) { // The player is being reset and this source will be released. return; @@ -861,8 +921,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Internal methods. private void startAdPlayback() { - ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); - AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); + AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings(); adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); adsRenderingSettings.setMimeTypes(supportedMimeTypes); if (mediaLoadTimeoutMs != TIMEOUT_UNSET) { @@ -951,11 +1010,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A imaPausedContent = true; pauseContentInternal(); break; - case STARTED: - if (ad.isSkippable()) { - focusSkipButton(); - } - break; case TAPPED: if (eventListener != null) { eventListener.onAdTapped(); @@ -978,6 +1032,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A handleAdGroupLoadError(new IOException(message)); } break; + case STARTED: case ALL_ADS_COMPLETED: default: break; @@ -1072,6 +1127,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (pendingAdLoadError == null) { pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex); } + // Discard the ad break, which makes sure we don't receive duplicate load error events. + adsManager.discardAdBreak(); + // Set the next expected ad group index so we can handle multiple load errors in a row. + adGroupIndex++; + if (adGroupIndex < adPlaybackState.adGroupCount) { + expectedAdGroupIndex = adGroupIndex; + } else { + expectedAdGroupIndex = C.INDEX_UNSET; + } + pendingContentPositionMs = C.TIME_UNSET; } private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) { @@ -1079,6 +1144,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.d( TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception); } + if (adsManager == null) { + Log.w(TAG, "Ignoring ad prepare error after release"); + return; + } if (imaAdState == IMA_AD_STATE_NONE) { // Send IMA a content position at the ad group so that it will try to play it, at which point // we can notify that it failed to load. @@ -1125,15 +1194,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } - private void focusSkipButton() { - if (playingAd && adUiViewGroup != null && adUiViewGroup.getChildCount() > 0 - && adUiViewGroup.getChildAt(0) instanceof WebView) { - WebView webView = (WebView) (adUiViewGroup.getChildAt(0)); - webView.requestFocus(); - webView.loadUrl(FOCUS_SKIP_BUTTON_WORKAROUND_JS); - } - } - /** * Returns the next ad index in the specified ad group to load, or {@link C#INDEX_UNSET} if all * ads in the ad group have loaded. @@ -1161,7 +1221,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.e(TAG, message, cause); // We can't recover from an unexpected error in general, so skip all remaining ads. if (adPlaybackState == null) { - adPlaybackState = new AdPlaybackState(); + adPlaybackState = AdPlaybackState.NONE; } else { for (int i = 0; i < adPlaybackState.adGroupCount; i++) { adPlaybackState = adPlaybackState.withSkippedAdGroup(i); @@ -1214,4 +1274,49 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return true; } } + + /** Factory for objects provided by the IMA SDK. */ + // @VisibleForTesting + /* package */ interface ImaFactory { + /** @see ImaSdkSettings */ + ImaSdkSettings createImaSdkSettings(); + /** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRenderingSettings() */ + AdsRenderingSettings createAdsRenderingSettings(); + /** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdDisplayContainer() */ + AdDisplayContainer createAdDisplayContainer(); + /** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() */ + AdsRequest createAdsRequest(); + /** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings) */ + com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( + Context context, ImaSdkSettings imaSdkSettings); + } + + /** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */ + private static final class DefaultImaFactory implements ImaFactory { + @Override + public ImaSdkSettings createImaSdkSettings() { + return ImaSdkFactory.getInstance().createImaSdkSettings(); + } + + @Override + public AdsRenderingSettings createAdsRenderingSettings() { + return ImaSdkFactory.getInstance().createAdsRenderingSettings(); + } + + @Override + public AdDisplayContainer createAdDisplayContainer() { + return ImaSdkFactory.getInstance().createAdDisplayContainer(); + } + + @Override + public AdsRequest createAdsRequest() { + return ImaSdkFactory.getInstance().createAdsRequest(); + } + + @Override + public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( + Context context, ImaSdkSettings imaSdkSettings) { + return ImaSdkFactory.getInstance().createAdsLoader(context, imaSdkSettings); + } + } } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index d3e1d9725e..400061d019 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -23,9 +23,11 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.SourceInfoRefreshListener; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.TransferListener; import java.io.IOException; /** @@ -34,12 +36,10 @@ import java.io.IOException; * @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader. */ @Deprecated -public final class ImaAdsMediaSource extends BaseMediaSource { +public final class ImaAdsMediaSource extends BaseMediaSource implements SourceInfoRefreshListener { private final AdsMediaSource adsMediaSource; - private SourceInfoRefreshListener adsMediaSourceListener; - /** * Constructs a new source that inserts ads linearly with the content specified by * {@code contentMediaSource}. @@ -77,16 +77,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource { } @Override - public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) { - adsMediaSourceListener = - new SourceInfoRefreshListener() { - @Override - public void onSourceInfoRefreshed( - MediaSource source, Timeline timeline, @Nullable Object manifest) { - refreshSourceInfo(timeline, manifest); - } - }; - adsMediaSource.prepareSource(player, isTopLevelSource, adsMediaSourceListener); + public void prepareSourceInternal( + final ExoPlayer player, + boolean isTopLevelSource, + @Nullable TransferListener mediaTransferListener) { + adsMediaSource.prepareSource( + player, isTopLevelSource, /* listener= */ this, mediaTransferListener); } @Override @@ -106,6 +102,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource { @Override public void releaseSourceInternal() { - adsMediaSource.releaseSource(adsMediaSourceListener); + adsMediaSource.releaseSource(/* listener= */ this); + } + + @Override + public void onSourceInfoRefreshed( + MediaSource source, Timeline timeline, @Nullable Object manifest) { + refreshSourceInfo(timeline, manifest); } } diff --git a/extensions/ima/src/test/AndroidManifest.xml b/extensions/ima/src/test/AndroidManifest.xml new file mode 100644 index 0000000000..9a4e33189e --- /dev/null +++ b/extensions/ima/src/test/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java new file mode 100644 index 0000000000..873a1b1d09 --- /dev/null +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import com.google.ads.interactivemedia.v3.api.Ad; +import com.google.ads.interactivemedia.v3.api.AdPodInfo; +import com.google.ads.interactivemedia.v3.api.CompanionAd; +import com.google.ads.interactivemedia.v3.api.UiElement; +import java.util.List; +import java.util.Set; + +/** A fake ad for testing. */ +/* package */ final class FakeAd implements Ad { + + private final boolean skippable; + private final AdPodInfo adPodInfo; + + public FakeAd(boolean skippable, int podIndex, int totalAds, int adPosition) { + this.skippable = skippable; + adPodInfo = + new AdPodInfo() { + @Override + public int getTotalAds() { + return totalAds; + } + + @Override + public int getAdPosition() { + return adPosition; + } + + @Override + public int getPodIndex() { + return podIndex; + } + + @Override + public boolean isBumper() { + throw new UnsupportedOperationException(); + } + + @Override + public double getMaxDuration() { + throw new UnsupportedOperationException(); + } + + @Override + public double getTimeOffset() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public boolean isSkippable() { + return skippable; + } + + @Override + public AdPodInfo getAdPodInfo() { + return adPodInfo; + } + + @Override + public String getAdId() { + throw new UnsupportedOperationException(); + } + + @Override + public String getCreativeId() { + throw new UnsupportedOperationException(); + } + + @Override + public String getCreativeAdId() { + throw new UnsupportedOperationException(); + } + + @Override + public String getUniversalAdIdValue() { + throw new UnsupportedOperationException(); + } + + @Override + public String getUniversalAdIdRegistry() { + throw new UnsupportedOperationException(); + } + + @Override + public String getAdSystem() { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getAdWrapperIds() { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getAdWrapperSystems() { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getAdWrapperCreativeIds() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isLinear() { + throw new UnsupportedOperationException(); + } + + @Override + public double getSkipTimeOffset() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isUiDisabled() { + throw new UnsupportedOperationException(); + } + + @Override + public String getDescription() { + throw new UnsupportedOperationException(); + } + + @Override + public String getTitle() { + throw new UnsupportedOperationException(); + } + + @Override + public String getContentType() { + throw new UnsupportedOperationException(); + } + + @Override + public String getAdvertiserName() { + throw new UnsupportedOperationException(); + } + + @Override + public String getSurveyUrl() { + throw new UnsupportedOperationException(); + } + + @Override + public String getDealId() { + throw new UnsupportedOperationException(); + } + + @Override + public int getWidth() { + throw new UnsupportedOperationException(); + } + + @Override + public int getHeight() { + throw new UnsupportedOperationException(); + } + + @Override + public String getTraffickingParameters() { + throw new UnsupportedOperationException(); + } + + @Override + public double getDuration() { + throw new UnsupportedOperationException(); + } + + @Override + public Set getUiElements() { + throw new UnsupportedOperationException(); + } + + @Override + public List getCompanionAds() { + throw new UnsupportedOperationException(); + } +} diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsLoader.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsLoader.java new file mode 100644 index 0000000000..a8f3daae33 --- /dev/null +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsLoader.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; +import com.google.ads.interactivemedia.v3.api.AdsManager; +import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; +import com.google.ads.interactivemedia.v3.api.AdsRequest; +import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; +import com.google.ads.interactivemedia.v3.api.StreamManager; +import com.google.ads.interactivemedia.v3.api.StreamRequest; +import com.google.android.exoplayer2.util.Assertions; +import java.util.ArrayList; + +/** Fake {@link com.google.ads.interactivemedia.v3.api.AdsLoader} implementation for tests. */ +public final class FakeAdsLoader implements com.google.ads.interactivemedia.v3.api.AdsLoader { + + private final ImaSdkSettings imaSdkSettings; + private final AdsManager adsManager; + private final ArrayList adsLoadedListeners; + private final ArrayList adErrorListeners; + + public FakeAdsLoader(ImaSdkSettings imaSdkSettings, AdsManager adsManager) { + this.imaSdkSettings = Assertions.checkNotNull(imaSdkSettings); + this.adsManager = Assertions.checkNotNull(adsManager); + adsLoadedListeners = new ArrayList<>(); + adErrorListeners = new ArrayList<>(); + } + + @Override + public void contentComplete() { + // Do nothing. + } + + @Override + public ImaSdkSettings getSettings() { + return imaSdkSettings; + } + + @Override + public void requestAds(AdsRequest adsRequest) { + for (AdsLoadedListener listener : adsLoadedListeners) { + listener.onAdsManagerLoaded( + new AdsManagerLoadedEvent() { + @Override + public AdsManager getAdsManager() { + return adsManager; + } + + @Override + public StreamManager getStreamManager() { + throw new UnsupportedOperationException(); + } + + @Override + public Object getUserRequestContext() { + return adsRequest.getUserRequestContext(); + } + }); + } + } + + @Override + public String requestStream(StreamRequest streamRequest) { + throw new UnsupportedOperationException(); + } + + @Override + public void addAdsLoadedListener(AdsLoadedListener adsLoadedListener) { + adsLoadedListeners.add(adsLoadedListener); + } + + @Override + public void removeAdsLoadedListener(AdsLoadedListener adsLoadedListener) { + adsLoadedListeners.remove(adsLoadedListener); + } + + @Override + public void addAdErrorListener(AdErrorListener adErrorListener) { + adErrorListeners.add(adErrorListener); + } + + @Override + public void removeAdErrorListener(AdErrorListener adErrorListener) { + adErrorListeners.remove(adErrorListener); + } +} diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java new file mode 100644 index 0000000000..7c2c8a6e0b --- /dev/null +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; +import com.google.ads.interactivemedia.v3.api.AdsRequest; +import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; +import java.util.List; +import java.util.Map; + +/** Fake {@link AdsRequest} implementation for tests. */ +public final class FakeAdsRequest implements AdsRequest { + + private String adTagUrl; + private String adsResponse; + private Object userRequestContext; + private AdDisplayContainer adDisplayContainer; + private ContentProgressProvider contentProgressProvider; + + @Override + public void setAdTagUrl(String adTagUrl) { + this.adTagUrl = adTagUrl; + } + + @Override + public String getAdTagUrl() { + return adTagUrl; + } + + @Override + public void setExtraParameter(String s, String s1) { + throw new UnsupportedOperationException(); + } + + @Override + public String getExtraParameter(String s) { + throw new UnsupportedOperationException(); + } + + @Override + public Map getExtraParameters() { + throw new UnsupportedOperationException(); + } + + @Override + public void setUserRequestContext(Object userRequestContext) { + this.userRequestContext = userRequestContext; + } + + @Override + public Object getUserRequestContext() { + return userRequestContext; + } + + @Override + public AdDisplayContainer getAdDisplayContainer() { + return adDisplayContainer; + } + + @Override + public void setAdDisplayContainer(AdDisplayContainer adDisplayContainer) { + this.adDisplayContainer = adDisplayContainer; + } + + @Override + public ContentProgressProvider getContentProgressProvider() { + return contentProgressProvider; + } + + @Override + public void setContentProgressProvider(ContentProgressProvider contentProgressProvider) { + this.contentProgressProvider = contentProgressProvider; + } + + @Override + public String getAdsResponse() { + return adsResponse; + } + + @Override + public void setAdsResponse(String adsResponse) { + this.adsResponse = adsResponse; + } + + @Override + public void setAdWillAutoPlay(boolean b) { + throw new UnsupportedOperationException(); + } + + @Override + public void setAdWillPlayMuted(boolean b) { + throw new UnsupportedOperationException(); + } + + @Override + public void setContentDuration(float v) { + throw new UnsupportedOperationException(); + } + + @Override + public void setContentKeywords(List list) { + throw new UnsupportedOperationException(); + } + + @Override + public void setContentTitle(String s) { + throw new UnsupportedOperationException(); + } + + @Override + public void setVastLoadTimeout(float v) { + throw new UnsupportedOperationException(); + } + + @Override + public void setLiveStreamPrefetchSeconds(float v) { + throw new UnsupportedOperationException(); + } +} diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java new file mode 100644 index 0000000000..11ed214279 --- /dev/null +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.testutil.StubExoPlayer; +import java.util.ArrayList; + +/** A fake player for testing content/ad playback. */ +/* package */ final class FakePlayer extends StubExoPlayer { + + private final ArrayList listeners; + private final Timeline.Window window; + private final Timeline.Period period; + + private boolean prepared; + private Timeline timeline; + private int state; + private boolean playWhenReady; + private long position; + private long contentPosition; + private boolean isPlayingAd; + private int adGroupIndex; + private int adIndexInAdGroup; + + public FakePlayer() { + listeners = new ArrayList<>(); + window = new Timeline.Window(); + period = new Timeline.Period(); + state = Player.STATE_IDLE; + playWhenReady = true; + timeline = Timeline.EMPTY; + } + + /** Sets the timeline on this fake player, which notifies listeners with the changed timeline. */ + public void updateTimeline(Timeline timeline) { + for (Player.EventListener listener : listeners) { + listener.onTimelineChanged( + timeline, + null, + prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED); + } + prepared = true; + } + + /** + * Sets the state of this player as if it were playing content at the given {@code position}. If + * an ad is currently playing, this will trigger a position discontinuity. + */ + public void setPlayingContentPosition(long position) { + boolean notify = isPlayingAd; + isPlayingAd = false; + adGroupIndex = C.INDEX_UNSET; + adIndexInAdGroup = C.INDEX_UNSET; + this.position = position; + contentPosition = position; + if (notify) { + for (Player.EventListener listener : listeners) { + listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION); + } + } + } + + /** + * Sets the state of this player as if it were playing an ad with the given indices at the given + * {@code position}. If the player is playing a different ad or content, this will trigger a + * position discontinuity. + */ + public void setPlayingAdPosition( + int adGroupIndex, int adIndexInAdGroup, long position, long contentPosition) { + boolean notify = !isPlayingAd || this.adIndexInAdGroup != adIndexInAdGroup; + isPlayingAd = true; + this.adGroupIndex = adGroupIndex; + this.adIndexInAdGroup = adIndexInAdGroup; + this.position = position; + this.contentPosition = contentPosition; + if (notify) { + for (Player.EventListener listener : listeners) { + listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION); + } + } + } + + /** Sets the state of this player with the given {@code STATE} constant. */ + public void setState(int state, boolean playWhenReady) { + boolean notify = this.state != state || this.playWhenReady != playWhenReady; + this.state = state; + this.playWhenReady = playWhenReady; + if (notify) { + for (Player.EventListener listener : listeners) { + listener.onPlayerStateChanged(playWhenReady, state); + } + } + } + + // ExoPlayer methods. Other methods are unsupported. + + @Override + public void addListener(Player.EventListener listener) { + listeners.add(listener); + } + + @Override + public void removeListener(Player.EventListener listener) { + listeners.remove(listener); + } + + @Override + public int getPlaybackState() { + return state; + } + + @Override + public boolean getPlayWhenReady() { + return playWhenReady; + } + + @Override + public Timeline getCurrentTimeline() { + return timeline; + } + + @Override + public int getCurrentPeriodIndex() { + return 0; + } + + @Override + public int getCurrentWindowIndex() { + return 0; + } + + @Override + public int getNextWindowIndex() { + return C.INDEX_UNSET; + } + + @Override + public int getPreviousWindowIndex() { + return C.INDEX_UNSET; + } + + @Override + public long getDuration() { + if (timeline.isEmpty()) { + return C.INDEX_UNSET; + } + if (isPlayingAd()) { + long adDurationUs = + timeline.getPeriod(0, period).getAdDurationUs(adGroupIndex, adIndexInAdGroup); + return C.usToMs(adDurationUs); + } else { + return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); + } + } + + @Override + public long getCurrentPosition() { + return position; + } + + @Override + public boolean isPlayingAd() { + return isPlayingAd; + } + + @Override + public int getCurrentAdGroupIndex() { + return adGroupIndex; + } + + @Override + public int getCurrentAdIndexInAdGroup() { + return adIndexInAdGroup; + } + + @Override + public long getContentPosition() { + return contentPosition; + } +} diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java new file mode 100644 index 0000000000..b0fe731480 --- /dev/null +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.net.Uri; +import android.support.annotation.Nullable; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import com.google.ads.interactivemedia.v3.api.Ad; +import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; +import com.google.ads.interactivemedia.v3.api.AdEvent; +import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType; +import com.google.ads.interactivemedia.v3.api.AdsManager; +import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; +import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.SinglePeriodTimeline; +import com.google.android.exoplayer2.source.ads.AdPlaybackState; +import com.google.android.exoplayer2.source.ads.AdsLoader; +import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; +import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline; +import com.google.android.exoplayer2.upstream.DataSpec; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +/** Test for {@link ImaAdsLoader}. */ +@RunWith(RobolectricTestRunner.class) +public class ImaAdsLoaderTest { + + private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND; + private static final Timeline CONTENT_TIMELINE = + new SinglePeriodTimeline(CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false); + private static final Uri TEST_URI = Uri.EMPTY; + private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND; + private static final long[][] PREROLL_ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}}; + private static final Float[] PREROLL_CUE_POINTS_SECONDS = new Float[] {0f}; + private static final FakeAd UNSKIPPABLE_AD = + new FakeAd(/* skippable= */ false, /* podIndex= */ 0, /* totalAds= */ 1, /* adPosition= */ 1); + + private @Mock ImaSdkSettings imaSdkSettings; + private @Mock AdsRenderingSettings adsRenderingSettings; + private @Mock AdDisplayContainer adDisplayContainer; + private @Mock AdsManager adsManager; + private SingletonImaFactory testImaFactory; + private ViewGroup adUiViewGroup; + private TestAdsLoaderListener adsLoaderListener; + private FakePlayer fakeExoPlayer; + private ImaAdsLoader imaAdsLoader; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + FakeAdsRequest fakeAdsRequest = new FakeAdsRequest(); + FakeAdsLoader fakeAdsLoader = new FakeAdsLoader(imaSdkSettings, adsManager); + testImaFactory = + new SingletonImaFactory( + imaSdkSettings, + adsRenderingSettings, + adDisplayContainer, + fakeAdsRequest, + fakeAdsLoader); + adUiViewGroup = new FrameLayout(RuntimeEnvironment.application); + } + + @After + public void teardown() { + if (imaAdsLoader != null) { + imaAdsLoader.release(); + } + } + + @Test + public void testBuilder_overridesPlayerType() { + when(imaSdkSettings.getPlayerType()).thenReturn("test player type"); + setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + + verify(imaSdkSettings).setPlayerType("google/exo.ext.ima"); + } + + @Test + public void testAttachPlayer_setsAdUiViewGroup() { + setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup); + + verify(adDisplayContainer, atLeastOnce()).setAdContainer(adUiViewGroup); + } + + @Test + public void testAttachPlayer_updatesAdPlaybackState() { + setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup); + + assertThat(adsLoaderListener.adPlaybackState) + .isEqualTo( + new AdPlaybackState(/* adGroupTimesUs= */ 0) + .withAdDurationsUs(PREROLL_ADS_DURATIONS_US)); + } + + @Test + public void testAttachAfterRelease() { + setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + imaAdsLoader.release(); + imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup); + } + + @Test + public void testAttachAndCallbacksAfterRelease() { + setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + imaAdsLoader.release(); + imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup); + fakeExoPlayer.setPlayingContentPosition(/* position= */ 0); + fakeExoPlayer.setState(Player.STATE_READY, true); + + // If callbacks are invoked there is no crash. + // Note: we can't currently call getContentProgress/getAdProgress as a VerifyError is thrown + // when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA + // SDK being proguarded. + imaAdsLoader.requestAds(adUiViewGroup); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD)); + imaAdsLoader.loadAd(TEST_URI.toString()); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD)); + imaAdsLoader.playAd(); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, UNSKIPPABLE_AD)); + imaAdsLoader.pauseAd(); + imaAdsLoader.stopAd(); + imaAdsLoader.onPlayerError(ExoPlaybackException.createForSource(new IOException())); + imaAdsLoader.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null)); + imaAdsLoader.handlePrepareError( + /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, new IOException()); + } + + @Test + public void testPlayback_withPrerollAd_marksAdAsPlayed() { + setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + + // Load the preroll ad. + imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD)); + imaAdsLoader.loadAd(TEST_URI.toString()); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD)); + + // Play the preroll ad. + imaAdsLoader.playAd(); + fakeExoPlayer.setPlayingAdPosition( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* position= */ 0, + /* contentPosition= */ 0); + fakeExoPlayer.setState(Player.STATE_READY, true); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, UNSKIPPABLE_AD)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, UNSKIPPABLE_AD)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.MIDPOINT, UNSKIPPABLE_AD)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, UNSKIPPABLE_AD)); + + // Play the content. + fakeExoPlayer.setPlayingContentPosition(0); + imaAdsLoader.stopAd(); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null)); + + // Verify that the preroll ad has been marked as played. + assertThat(adsLoaderListener.adPlaybackState) + .isEqualTo( + new AdPlaybackState(/* adGroupTimesUs= */ 0) + .withContentDurationUs(CONTENT_DURATION_US) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* uri= */ TEST_URI) + .withAdDurationsUs(PREROLL_ADS_DURATIONS_US) + .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .withAdResumePositionUs(/* adResumePositionUs= */ 0)); + } + + private void setupPlayback(Timeline contentTimeline, long[][] adDurationsUs, Float[] cuePoints) { + fakeExoPlayer = new FakePlayer(); + adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs); + when(adsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints)); + imaAdsLoader = + new ImaAdsLoader.Builder(RuntimeEnvironment.application) + .setImaFactory(testImaFactory) + .setImaSdkSettings(imaSdkSettings) + .buildForAdTag(TEST_URI); + } + + private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) { + return new AdEvent() { + @Override + public AdEventType getType() { + return adEventType; + } + + @Override + public @Nullable Ad getAd() { + return ad; + } + + @Override + public Map getAdData() { + return Collections.emptyMap(); + } + }; + } + + /** Ad loader event listener that forwards ad playback state to a fake player. */ + private static final class TestAdsLoaderListener implements AdsLoader.EventListener { + + private final FakePlayer fakeExoPlayer; + private final Timeline contentTimeline; + private final long[][] adDurationsUs; + + public AdPlaybackState adPlaybackState; + + public TestAdsLoaderListener( + FakePlayer fakeExoPlayer, Timeline contentTimeline, long[][] adDurationsUs) { + this.fakeExoPlayer = fakeExoPlayer; + this.contentTimeline = contentTimeline; + this.adDurationsUs = adDurationsUs; + } + + @Override + public void onAdPlaybackState(AdPlaybackState adPlaybackState) { + adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs); + this.adPlaybackState = adPlaybackState; + fakeExoPlayer.updateTimeline(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState)); + } + + @Override + public void onAdLoadError(AdLoadException error, DataSpec dataSpec) { + assertThat(error.type).isNotEqualTo(AdLoadException.TYPE_UNEXPECTED); + } + + @Override + public void onAdClicked() { + // Do nothing. + } + + @Override + public void onAdTapped() { + // Do nothing. + } + } +} diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java new file mode 100644 index 0000000000..dd46d8a68b --- /dev/null +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import android.content.Context; +import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; +import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; +import com.google.ads.interactivemedia.v3.api.AdsRequest; +import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; + +/** {@link ImaAdsLoader.ImaFactory} that returns provided instances from each getter, for tests. */ +final class SingletonImaFactory implements ImaAdsLoader.ImaFactory { + + private final ImaSdkSettings imaSdkSettings; + private final AdsRenderingSettings adsRenderingSettings; + private final AdDisplayContainer adDisplayContainer; + private final AdsRequest adsRequest; + private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; + + public SingletonImaFactory( + ImaSdkSettings imaSdkSettings, + AdsRenderingSettings adsRenderingSettings, + AdDisplayContainer adDisplayContainer, + AdsRequest adsRequest, + com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader) { + this.imaSdkSettings = imaSdkSettings; + this.adsRenderingSettings = adsRenderingSettings; + this.adDisplayContainer = adDisplayContainer; + this.adsRequest = adsRequest; + this.adsLoader = adsLoader; + } + + @Override + public ImaSdkSettings createImaSdkSettings() { + return imaSdkSettings; + } + + @Override + public AdsRenderingSettings createAdsRenderingSettings() { + return adsRenderingSettings; + } + + @Override + public AdDisplayContainer createAdDisplayContainer() { + return adDisplayContainer; + } + + @Override + public AdsRequest createAdsRequest() { + return adsRequest; + } + + @Override + public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( + Context context, ImaSdkSettings imaSdkSettings) { + return adsLoader; + } +} diff --git a/extensions/ima/src/test/resources/robolectric.properties b/extensions/ima/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..2f3210368e --- /dev/null +++ b/extensions/ima/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +manifest=src/test/AndroidManifest.xml diff --git a/extensions/jobdispatcher/build.gradle b/extensions/jobdispatcher/build.gradle index f4a8751c67..a0e3f8e0c8 100644 --- a/extensions/jobdispatcher/build.gradle +++ b/extensions/jobdispatcher/build.gradle @@ -20,6 +20,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java index c6701da964..f75607f268 100644 --- a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java +++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java @@ -29,6 +29,7 @@ import com.firebase.jobdispatcher.JobService; import com.firebase.jobdispatcher.Lifetime; import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.Scheduler; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; /** @@ -146,11 +147,14 @@ public final class JobDispatcherScheduler implements Scheduler { public boolean onStartJob(JobParameters params) { logd("JobDispatcherSchedulerService is started"); Bundle extras = params.getExtras(); + Assertions.checkNotNull(extras, "Service started without extras."); Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS)); if (requirements.checkRequirements(this)) { logd("Requirements are met"); String serviceAction = extras.getString(KEY_SERVICE_ACTION); String servicePackage = extras.getString(KEY_SERVICE_PACKAGE); + Assertions.checkNotNull(serviceAction, "Service action missing."); + Assertions.checkNotNull(servicePackage, "Service package missing."); Intent intent = new Intent(serviceAction).setPackage(servicePackage); logd("Starting service action: " + serviceAction + " package: " + servicePackage); Util.startForegroundService(this, intent); diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle index dc187a5709..10bfef8e7c 100644 --- a/extensions/leanback/build.gradle +++ b/extensions/leanback/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion 17 targetSdkVersion project.ext.targetSdkVersion diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index 03f53c263f..0c9491bb1a 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -39,7 +39,7 @@ import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.video.VideoListener; /** Leanback {@code PlayerAdapter} implementation for {@link Player}. */ -public final class LeanbackPlayerAdapter extends PlayerAdapter { +public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnable { static { ExoPlayerLibraryInfo.registerModule("goog.exo.leanback"); @@ -49,12 +49,12 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { private final Player player; private final Handler handler; private final ComponentListener componentListener; - private final Runnable updateProgressRunnable; + private final int updatePeriodMs; private @Nullable PlaybackPreparer playbackPreparer; private ControlDispatcher controlDispatcher; private @Nullable ErrorMessageProvider errorMessageProvider; - private SurfaceHolderGlueHost surfaceHolderGlueHost; + private @Nullable SurfaceHolderGlueHost surfaceHolderGlueHost; private boolean hasSurface; private boolean lastNotifiedPreparedState; @@ -70,18 +70,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { public LeanbackPlayerAdapter(Context context, Player player, final int updatePeriodMs) { this.context = context; this.player = player; + this.updatePeriodMs = updatePeriodMs; handler = new Handler(); componentListener = new ComponentListener(); controlDispatcher = new DefaultControlDispatcher(); - updateProgressRunnable = new Runnable() { - @Override - public void run() { - Callback callback = getCallback(); - callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this); - callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this); - handler.postDelayed(this, updatePeriodMs); - } - }; } /** @@ -138,7 +130,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { videoComponent.removeVideoListener(componentListener); } if (surfaceHolderGlueHost != null) { - surfaceHolderGlueHost.setSurfaceHolderCallback(null); + removeSurfaceHolderCallback(surfaceHolderGlueHost); surfaceHolderGlueHost = null; } hasSurface = false; @@ -150,9 +142,9 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { @Override public void setProgressUpdatingEnabled(boolean enabled) { - handler.removeCallbacks(updateProgressRunnable); + handler.removeCallbacks(this); if (enabled) { - handler.post(updateProgressRunnable); + handler.post(this); } } @@ -211,9 +203,19 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { && (surfaceHolderGlueHost == null || hasSurface); } + // Runnable implementation. + + @Override + public void run() { + Callback callback = getCallback(); + callback.onCurrentPositionChanged(this); + callback.onBufferedPositionChanged(this); + handler.postDelayed(this, updatePeriodMs); + } + // Internal methods. - /* package */ void setVideoSurface(Surface surface) { + /* package */ void setVideoSurface(@Nullable Surface surface) { hasSurface = surface != null; Player.VideoComponent videoComponent = player.getVideoComponent(); if (videoComponent != null) { @@ -241,8 +243,13 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } } - private final class ComponentListener extends Player.DefaultEventListener - implements SurfaceHolder.Callback, VideoListener { + @SuppressWarnings("nullness:argument.type.incompatible") + private static void removeSurfaceHolderCallback(SurfaceHolderGlueHost surfaceHolderGlueHost) { + surfaceHolderGlueHost.setSurfaceHolderCallback(null); + } + + private final class ComponentListener + implements Player.EventListener, SurfaceHolder.Callback, VideoListener { // SurfaceHolder.Callback implementation. @@ -281,8 +288,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } @Override - public void onTimelineChanged(Timeline timeline, Object manifest, - @TimelineChangeReason int reason) { + public void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { Callback callback = getCallback(); callback.onDurationChanged(LeanbackPlayerAdapter.this); callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this); diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle index eaaf078b5c..da04b0aec3 100644 --- a/extensions/mediasession/build.gradle +++ b/extensions/mediasession/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java index ce597b45cd..7d983e14e9 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 The Android Open Source Project + * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -127,7 +127,7 @@ public class DefaultPlaybackController implements MediaSessionConnector.Playback @Override public void onStop(Player player) { - player.stop(); + player.stop(true); } @Override diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 83fb16236d..c3d6a13f46 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -19,7 +19,6 @@ import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.os.Handler; -import android.os.Looper; import android.os.ResultReceiver; import android.os.SystemClock; import android.support.annotation.NonNull; @@ -39,6 +38,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.util.RepeatModeUtil; +import com.google.android.exoplayer2.util.Util; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -46,25 +46,26 @@ import java.util.Map; /** * Connects a {@link MediaSessionCompat} to a {@link Player}. - *

- * The connector listens for actions sent by the media session's controller and implements these + * + *

The connector listens for actions sent by the media session's controller and implements these * actions by calling appropriate player methods. The playback state of the media session is * automatically synced with the player. The connector can also be optionally extended by providing * various collaborators: + * *

    - *
  • Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and - * {@code PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed - * when calling {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. Custom - * actions can be handled by passing one or more {@link CustomActionProvider}s in a similar way. - *
  • + *
  • Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and {@code + * PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed + * when calling {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. Custom + * actions can be handled by passing one or more {@link CustomActionProvider}s in a similar + * way. *
  • To enable a media queue and navigation within it, you can set a {@link QueueNavigator} by - * calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator} is - * recommended for most use cases.
  • - *
  • To enable editing of the media queue, you can set a {@link QueueEditor} by calling - * {@link #setQueueEditor(QueueEditor)}.
  • + * calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator} + * is recommended for most use cases. + *
  • To enable editing of the media queue, you can set a {@link QueueEditor} by calling {@link + * #setQueueEditor(QueueEditor)}. *
  • An {@link ErrorMessageProvider} for providing human readable error messages and - * corresponding error codes can be set by calling - * {@link #setErrorMessageProvider(ErrorMessageProvider)}.
  • + * corresponding error codes can be set by calling {@link + * #setErrorMessageProvider(ErrorMessageProvider)}. *
*/ public final class MediaSessionConnector { @@ -74,35 +75,30 @@ public final class MediaSessionConnector { } /** - * The default repeat toggle modes which is the bitmask of - * {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ONE} and - * {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ALL}. + * The default repeat toggle modes which is the bitmask of {@link + * RepeatModeUtil#REPEAT_TOGGLE_MODE_ONE} and {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ALL}. */ public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL; - public static final String EXTRAS_PITCH = "EXO_PITCH"; - private static final int BASE_MEDIA_SESSION_FLAGS = MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS - | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS; - private static final int EDITOR_MEDIA_SESSION_FLAGS = BASE_MEDIA_SESSION_FLAGS - | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; - /** - * Receiver of media commands sent by a media controller. - */ + public static final String EXTRAS_PITCH = "EXO_PITCH"; + private static final int BASE_MEDIA_SESSION_FLAGS = + MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS + | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS; + private static final int EDITOR_MEDIA_SESSION_FLAGS = + BASE_MEDIA_SESSION_FLAGS | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; + + /** Receiver of media commands sent by a media controller. */ public interface CommandReceiver { /** * Returns the commands the receiver handles, or {@code null} if no commands need to be handled. */ String[] getCommands(); - /** - * See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. - */ + /** See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. */ void onCommand(Player player, String command, Bundle extras, ResultReceiver cb); } - /** - * Interface to which playback preparation actions are delegated. - */ + /** Interface to which playback preparation actions are delegated. */ public interface PlaybackPreparer extends CommandReceiver { long ACTIONS = @@ -127,96 +123,77 @@ public final class MediaSessionConnector { * @return The bitmask of the supported media actions. */ long getSupportedPrepareActions(); - /** - * See {@link MediaSessionCompat.Callback#onPrepare()}. - */ + /** See {@link MediaSessionCompat.Callback#onPrepare()}. */ void onPrepare(); - /** - * See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. - */ + /** See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. */ void onPrepareFromMediaId(String mediaId, Bundle extras); - /** - * See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. - */ + /** See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. */ void onPrepareFromSearch(String query, Bundle extras); - /** - * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. - */ + /** See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. */ void onPrepareFromUri(Uri uri, Bundle extras); } - /** - * Interface to which playback actions are delegated. - */ + /** Interface to which playback actions are delegated. */ public interface PlaybackController extends CommandReceiver { - long ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY - | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SEEK_TO - | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND - | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SET_REPEAT_MODE - | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE; + long ACTIONS = + PlaybackStateCompat.ACTION_PLAY_PAUSE + | PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_PAUSE + | PlaybackStateCompat.ACTION_SEEK_TO + | PlaybackStateCompat.ACTION_FAST_FORWARD + | PlaybackStateCompat.ACTION_REWIND + | PlaybackStateCompat.ACTION_STOP + | PlaybackStateCompat.ACTION_SET_REPEAT_MODE + | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE; /** * Returns the actions which are supported by the controller. The supported actions must be a - * bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE}, - * {@link PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE}, - * {@link PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD}, - * {@link PlaybackStateCompat#ACTION_REWIND}, {@link PlaybackStateCompat#ACTION_STOP}, - * {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE} and - * {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE}. + * bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE}, {@link + * PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE}, {@link + * PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD}, {@link + * PlaybackStateCompat#ACTION_REWIND}, {@link PlaybackStateCompat#ACTION_STOP}, {@link + * PlaybackStateCompat#ACTION_SET_REPEAT_MODE} and {@link + * PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE}. * * @param player The player. * @return The bitmask of the supported media actions. */ long getSupportedPlaybackActions(@Nullable Player player); - /** - * See {@link MediaSessionCompat.Callback#onPlay()}. - */ + /** See {@link MediaSessionCompat.Callback#onPlay()}. */ void onPlay(Player player); - /** - * See {@link MediaSessionCompat.Callback#onPause()}. - */ + /** See {@link MediaSessionCompat.Callback#onPause()}. */ void onPause(Player player); - /** - * See {@link MediaSessionCompat.Callback#onSeekTo(long)}. - */ + /** See {@link MediaSessionCompat.Callback#onSeekTo(long)}. */ void onSeekTo(Player player, long position); - /** - * See {@link MediaSessionCompat.Callback#onFastForward()}. - */ + /** See {@link MediaSessionCompat.Callback#onFastForward()}. */ void onFastForward(Player player); - /** - * See {@link MediaSessionCompat.Callback#onRewind()}. - */ + /** See {@link MediaSessionCompat.Callback#onRewind()}. */ void onRewind(Player player); - /** - * See {@link MediaSessionCompat.Callback#onStop()}. - */ + /** See {@link MediaSessionCompat.Callback#onStop()}. */ void onStop(Player player); - /** - * See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}. - */ + /** See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}. */ void onSetShuffleMode(Player player, int shuffleMode); - /** - * See {@link MediaSessionCompat.Callback#onSetRepeatMode(int)}. - */ + /** See {@link MediaSessionCompat.Callback#onSetRepeatMode(int)}. */ void onSetRepeatMode(Player player, int repeatMode); } /** - * Handles queue navigation actions, and updates the media session queue by calling - * {@code MediaSessionCompat.setQueue()}. + * Handles queue navigation actions, and updates the media session queue by calling {@code + * MediaSessionCompat.setQueue()}. */ public interface QueueNavigator extends CommandReceiver { - long ACTIONS = PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM - | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; + long ACTIONS = + PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM + | PlaybackStateCompat.ACTION_SKIP_TO_NEXT + | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; /** * Returns the actions which are supported by the navigator. The supported actions must be a - * bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM}, - * {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}, - * {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}. + * bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM}, {@link + * PlaybackStateCompat#ACTION_SKIP_TO_NEXT}, {@link + * PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}. * * @param player The {@link Player}. * @return The bitmask of the supported media actions. @@ -235,34 +212,26 @@ public final class MediaSessionConnector { */ void onCurrentWindowIndexChanged(Player player); /** - * Gets the id of the currently active queue item, or - * {@link MediaSessionCompat.QueueItem#UNKNOWN_ID} if the active item is unknown. - *

- * To let the connector publish metadata for the active queue item, the queue item with the - * returned id must be available in the list of items returned by - * {@link MediaControllerCompat#getQueue()}. + * Gets the id of the currently active queue item, or {@link + * MediaSessionCompat.QueueItem#UNKNOWN_ID} if the active item is unknown. + * + *

To let the connector publish metadata for the active queue item, the queue item with the + * returned id must be available in the list of items returned by {@link + * MediaControllerCompat#getQueue()}. * * @param player The player connected to the media session. * @return The id of the active queue item. */ long getActiveQueueItemId(@Nullable Player player); - /** - * See {@link MediaSessionCompat.Callback#onSkipToPrevious()}. - */ + /** See {@link MediaSessionCompat.Callback#onSkipToPrevious()}. */ void onSkipToPrevious(Player player); - /** - * See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}. - */ + /** See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}. */ void onSkipToQueueItem(Player player, long id); - /** - * See {@link MediaSessionCompat.Callback#onSkipToNext()}. - */ + /** See {@link MediaSessionCompat.Callback#onSkipToNext()}. */ void onSkipToNext(Player player); } - /** - * Handles media session queue edits. - */ + /** Handles media session queue edits. */ public interface QueueEditor extends CommandReceiver { /** @@ -270,8 +239,8 @@ public final class MediaSessionConnector { */ void onAddQueueItem(Player player, MediaDescriptionCompat description); /** - * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description, - * int index)}. + * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description, int + * index)}. */ void onAddQueueItem(Player player, MediaDescriptionCompat description, int index); /** @@ -279,9 +248,7 @@ public final class MediaSessionConnector { * description)}. */ void onRemoveQueueItem(Player player, MediaDescriptionCompat description); - /** - * See {@link MediaSessionCompat.Callback#onRemoveQueueItemAt(int index)}. - */ + /** See {@link MediaSessionCompat.Callback#onRemoveQueueItemAt(int index)}. */ void onRemoveQueueItemAt(Player player, int index); } @@ -308,43 +275,49 @@ public final class MediaSessionConnector { void onCustomAction(String action, Bundle extras); /** - * Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the - * media session by the connector or {@code null} if this action should not be published at the - * given player state. + * Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the media + * session by the connector or {@code null} if this action should not be published at the given + * player state. * * @return The custom action to be included in the session playback state or {@code null}. */ PlaybackStateCompat.CustomAction getCustomAction(); } - /** - * The wrapped {@link MediaSessionCompat}. - */ + /** Provides a {@link MediaMetadataCompat} for a given player state. */ + public interface MediaMetadataProvider { + /** + * Gets the {@link MediaMetadataCompat} to be published to the session. + * + * @param player The player for which to provide metadata. + * @return The {@link MediaMetadataCompat} to be published to the session. + */ + MediaMetadataCompat getMetadata(Player player); + } + + /** The wrapped {@link MediaSessionCompat}. */ public final MediaSessionCompat mediaSession; - private final MediaControllerCompat mediaController; - private final Handler handler; - private final boolean doMaintainMetadata; + private @Nullable final MediaMetadataProvider mediaMetadataProvider; private final ExoPlayerEventListener exoPlayerEventListener; private final MediaSessionCallback mediaSessionCallback; private final PlaybackController playbackController; - private final String metadataExtrasPrefix; private final Map commandMap; private Player player; private CustomActionProvider[] customActionProviders; private Map customActionMap; private @Nullable ErrorMessageProvider errorMessageProvider; + private @Nullable Pair customError; private PlaybackPreparer playbackPreparer; private QueueNavigator queueNavigator; private QueueEditor queueEditor; private RatingCallback ratingCallback; /** - * Creates an instance. Must be called on the same thread that is used to construct the player - * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. - *

- * Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}. + * Creates an instance. + * + *

Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. */ @@ -353,17 +326,46 @@ public final class MediaSessionConnector { } /** - * Creates an instance. Must be called on the same thread that is used to construct the player - * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. + * Creates an instance. * - *

Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true, null)}. + *

Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, new + * DefaultMediaMetadataProvider(mediaSession.getController(), null))}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. * @param playbackController A {@link PlaybackController} for handling playback actions. */ public MediaSessionConnector( MediaSessionCompat mediaSession, PlaybackController playbackController) { - this(mediaSession, playbackController, true, null); + this( + mediaSession, + playbackController, + new DefaultMediaMetadataProvider(mediaSession.getController(), null)); + } + + /** + * Creates an instance. + * + * @param mediaSession The {@link MediaSessionCompat} to connect to. + * @param playbackController A {@link PlaybackController} for handling playback actions, or {@code + * null} if the connector should handle playback actions directly. + * @param doMaintainMetadata Whether the connector should maintain the metadata of the session. + * @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the active + * queue item to the session metadata. + * @deprecated Use {@link MediaSessionConnector#MediaSessionConnector(MediaSessionCompat, + * PlaybackController, MediaMetadataProvider)}. + */ + @Deprecated + public MediaSessionConnector( + MediaSessionCompat mediaSession, + @Nullable PlaybackController playbackController, + boolean doMaintainMetadata, + @Nullable String metadataExtrasPrefix) { + this( + mediaSession, + playbackController, + doMaintainMetadata + ? new DefaultMediaMetadataProvider(mediaSession.getController(), metadataExtrasPrefix) + : null); } /** @@ -373,26 +375,19 @@ public final class MediaSessionConnector { * @param mediaSession The {@link MediaSessionCompat} to connect to. * @param playbackController A {@link PlaybackController} for handling playback actions, or {@code * null} if the connector should handle playback actions directly. - * @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If - * {@code false}, you need to maintain the metadata of the media session yourself (provide at - * least the duration to allow clients to show a progress bar). - * @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the active - * queue item to the session metadata. + * @param mediaMetadataProvider A {@link MediaMetadataProvider} for providing a custom metadata + * object to be published to the media session, or {@code null} if metadata shouldn't be + * published. */ public MediaSessionConnector( MediaSessionCompat mediaSession, - PlaybackController playbackController, - boolean doMaintainMetadata, - @Nullable String metadataExtrasPrefix) { + @Nullable PlaybackController playbackController, + @Nullable MediaMetadataProvider mediaMetadataProvider) { this.mediaSession = mediaSession; - this.playbackController = playbackController != null ? playbackController - : new DefaultPlaybackController(); - this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : ""; - this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper() - : Looper.getMainLooper()); - this.doMaintainMetadata = doMaintainMetadata; + this.playbackController = + playbackController != null ? playbackController : new DefaultPlaybackController(); + this.mediaMetadataProvider = mediaMetadataProvider; mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS); - mediaController = mediaSession.getController(); mediaSessionCallback = new MediaSessionCallback(); exoPlayerEventListener = new ExoPlayerEventListener(); customActionMap = Collections.emptyMap(); @@ -401,7 +396,8 @@ public final class MediaSessionConnector { } /** - * Sets the player to be connected to the media session. + * Sets the player to be connected to the media session. Must be called on the same thread that is + * used to access the player. * *

The order in which any {@link CustomActionProvider}s are passed determines the order of the * actions published with the playback state of the session. @@ -425,14 +421,17 @@ public final class MediaSessionConnector { this.playbackPreparer = playbackPreparer; registerCommandReceiver(playbackPreparer); - this.customActionProviders = (player != null && customActionProviders != null) - ? customActionProviders : new CustomActionProvider[0]; + this.customActionProviders = + (player != null && customActionProviders != null) + ? customActionProviders + : new CustomActionProvider[0]; if (player != null) { + Handler handler = new Handler(Util.getLooper()); mediaSession.setCallback(mediaSessionCallback, handler); player.addListener(exoPlayerEventListener); } - updateMediaSessionPlaybackState(); - updateMediaSessionMetadata(); + invalidateMediaSessionPlaybackState(); + invalidateMediaSessionMetadata(); } /** @@ -444,7 +443,7 @@ public final class MediaSessionConnector { @Nullable ErrorMessageProvider errorMessageProvider) { if (this.errorMessageProvider != errorMessageProvider) { this.errorMessageProvider = errorMessageProvider; - updateMediaSessionPlaybackState(); + invalidateMediaSessionPlaybackState(); } } @@ -490,23 +489,50 @@ public final class MediaSessionConnector { } } - private void registerCommandReceiver(CommandReceiver commandReceiver) { - if (commandReceiver != null && commandReceiver.getCommands() != null) { - for (String command : commandReceiver.getCommands()) { - commandMap.put(command, commandReceiver); - } + /** + * Sets a custom error on the session. + * + *

This sets the error code via {@link PlaybackStateCompat.Builder#setErrorMessage(int, + * CharSequence)}. By default, the error code will be set to {@link + * PlaybackStateCompat#ERROR_CODE_APP_ERROR}. + * + * @param message The error string to report or {@code null} to clear the error. + */ + public void setCustomErrorMessage(@Nullable CharSequence message) { + int code = (message == null) ? 0 : PlaybackStateCompat.ERROR_CODE_APP_ERROR; + setCustomErrorMessage(message, code); + } + + /** + * Sets a custom error on the session. + * + * @param message The error string to report or {@code null} to clear the error. + * @param code The error code to report. Ignored when {@code message} is {@code null}. + */ + public void setCustomErrorMessage(@Nullable CharSequence message, int code) { + customError = (message == null) ? null : new Pair<>(code, message); + invalidateMediaSessionPlaybackState(); + } + + /** + * Updates the metadata of the media session. + * + *

Apps normally only need to call this method when the backing data for a given media item has + * changed and the metadata should be updated immediately. + */ + public final void invalidateMediaSessionMetadata() { + if (mediaMetadataProvider != null && player != null) { + mediaSession.setMetadata(mediaMetadataProvider.getMetadata(player)); } } - private void unregisterCommandReceiver(CommandReceiver commandReceiver) { - if (commandReceiver != null && commandReceiver.getCommands() != null) { - for (String command : commandReceiver.getCommands()) { - commandMap.remove(command); - } - } - } - - private void updateMediaSessionPlaybackState() { + /** + * Updates the playback state of the media session. + * + *

Apps normally only need to call this method when the custom actions provided by a {@link + * CustomActionProvider} changed and the playback state needs to be updated immediately. + */ + public final void invalidateMediaSessionPlaybackState() { PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); if (player == null) { builder.setActions(buildPlaybackActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0); @@ -527,36 +553,74 @@ public final class MediaSessionConnector { int playbackState = player.getPlaybackState(); ExoPlaybackException playbackError = playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null; + boolean reportError = playbackError != null || customError != null; int sessionPlaybackState = - playbackError != null + reportError ? PlaybackStateCompat.STATE_ERROR : mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady()); - if (playbackError != null && errorMessageProvider != null) { + if (customError != null) { + builder.setErrorMessage(customError.first, customError.second); + } else if (playbackError != null && errorMessageProvider != null) { Pair message = errorMessageProvider.getErrorMessage(playbackError); builder.setErrorMessage(message.first, message.second); } - long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player) - : MediaSessionCompat.QueueItem.UNKNOWN_ID; + long activeQueueItemId = + queueNavigator != null + ? queueNavigator.getActiveQueueItemId(player) + : MediaSessionCompat.QueueItem.UNKNOWN_ID; Bundle extras = new Bundle(); extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch); - builder.setActions(buildPlaybackActions()) + builder + .setActions(buildPlaybackActions()) .setActiveQueueItemId(activeQueueItemId) .setBufferedPosition(player.getBufferedPosition()) - .setState(sessionPlaybackState, player.getCurrentPosition(), - player.getPlaybackParameters().speed, SystemClock.elapsedRealtime()) + .setState( + sessionPlaybackState, + player.getCurrentPosition(), + player.getPlaybackParameters().speed, + SystemClock.elapsedRealtime()) .setExtras(extras); mediaSession.setPlaybackState(builder.build()); } + /** + * Updates the queue of the media session by calling {@link + * QueueNavigator#onTimelineChanged(Player)}. + * + *

Apps normally only need to call this method when the backing data for a given queue item has + * changed and the queue should be updated immediately. + */ + public final void invalidateMediaSessionQueue() { + if (queueNavigator != null && player != null) { + queueNavigator.onTimelineChanged(player); + } + } + + private void registerCommandReceiver(CommandReceiver commandReceiver) { + if (commandReceiver != null && commandReceiver.getCommands() != null) { + for (String command : commandReceiver.getCommands()) { + commandMap.put(command, commandReceiver); + } + } + } + + private void unregisterCommandReceiver(CommandReceiver commandReceiver) { + if (commandReceiver != null && commandReceiver.getCommands() != null) { + for (String command : commandReceiver.getCommands()) { + commandMap.remove(command); + } + } + } + private long buildPlaybackActions() { - long actions = (PlaybackController.ACTIONS - & playbackController.getSupportedPlaybackActions(player)); + long actions = + (PlaybackController.ACTIONS & playbackController.getSupportedPlaybackActions(player)); if (playbackPreparer != null) { actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions()); } if (queueNavigator != null) { - actions |= (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions( - player)); + actions |= + (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(player)); } if (ratingCallback != null) { actions |= RatingCallback.ACTIONS; @@ -564,17 +628,79 @@ public final class MediaSessionConnector { return actions; } - private void updateMediaSessionMetadata() { - if (doMaintainMetadata) { + private int mapPlaybackState(int exoPlayerPlaybackState, boolean playWhenReady) { + switch (exoPlayerPlaybackState) { + case Player.STATE_BUFFERING: + return PlaybackStateCompat.STATE_BUFFERING; + case Player.STATE_READY: + return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED; + case Player.STATE_ENDED: + return PlaybackStateCompat.STATE_PAUSED; + default: + return PlaybackStateCompat.STATE_NONE; + } + } + + private boolean canDispatchToPlaybackPreparer(long action) { + return playbackPreparer != null + && (playbackPreparer.getSupportedPrepareActions() & PlaybackPreparer.ACTIONS & action) != 0; + } + + private boolean canDispatchToRatingCallback(long action) { + return ratingCallback != null && (RatingCallback.ACTIONS & action) != 0; + } + + private boolean canDispatchToPlaybackController(long action) { + return (playbackController.getSupportedPlaybackActions(player) + & PlaybackController.ACTIONS + & action) + != 0; + } + + private boolean canDispatchToQueueNavigator(long action) { + return queueNavigator != null + && (queueNavigator.getSupportedQueueNavigatorActions(player) + & QueueNavigator.ACTIONS + & action) + != 0; + } + + /** + * Provides a default {@link MediaMetadataCompat} with properties and extras propagated from the + * active queue item to the session metadata. + */ + public static final class DefaultMediaMetadataProvider implements MediaMetadataProvider { + + private final MediaControllerCompat mediaController; + private final String metadataExtrasPrefix; + + /** + * Creates a new instance. + * + * @param mediaController The {@link MediaControllerCompat}. + * @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the + * active queue item to the session metadata. + */ + public DefaultMediaMetadataProvider( + MediaControllerCompat mediaController, @Nullable String metadataExtrasPrefix) { + this.mediaController = mediaController; + this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : ""; + } + + @Override + public MediaMetadataCompat getMetadata(Player player) { + if (player.getCurrentTimeline().isEmpty()) { + return null; + } MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder(); - if (player != null && player.isPlayingAd()) { + if (player.isPlayingAd()) { builder.putLong(MediaMetadataCompat.METADATA_KEY_ADVERTISEMENT, 1); } - builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, player == null ? 0 - : player.getDuration() == C.TIME_UNSET ? -1 : player.getDuration()); - - if (queueNavigator != null) { - long activeQueueItemId = queueNavigator.getActiveQueueItemId(player); + builder.putLong( + MediaMetadataCompat.METADATA_KEY_DURATION, + player.getDuration() == C.TIME_UNSET ? -1 : player.getDuration()); + long activeQueueItemId = mediaController.getPlaybackState().getActiveQueueItemId(); + if (activeQueueItemId != MediaSessionCompat.QueueItem.UNKNOWN_ID) { List queue = mediaController.getQueue(); for (int i = 0; queue != null && i < queue.size(); i++) { MediaSessionCompat.QueueItem queueItem = queue.get(i); @@ -600,113 +726,92 @@ public final class MediaSessionConnector { } } if (description.getTitle() != null) { - builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, - String.valueOf(description.getTitle())); + String title = String.valueOf(description.getTitle()); + builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title); + builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title); } if (description.getSubtitle() != null) { - builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, + builder.putString( + MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, String.valueOf(description.getSubtitle())); } if (description.getDescription() != null) { - builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, + builder.putString( + MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, String.valueOf(description.getDescription())); } if (description.getIconBitmap() != null) { - builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, - description.getIconBitmap()); + builder.putBitmap( + MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, description.getIconBitmap()); } if (description.getIconUri() != null) { - builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, + builder.putString( + MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, String.valueOf(description.getIconUri())); } if (description.getMediaId() != null) { - builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, + builder.putString( + MediaMetadataCompat.METADATA_KEY_MEDIA_ID, String.valueOf(description.getMediaId())); } if (description.getMediaUri() != null) { - builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, + builder.putString( + MediaMetadataCompat.METADATA_KEY_MEDIA_URI, String.valueOf(description.getMediaUri())); } break; } } } - mediaSession.setMetadata(builder.build()); + return builder.build(); } } - private int mapPlaybackState(int exoPlayerPlaybackState, boolean playWhenReady) { - switch (exoPlayerPlaybackState) { - case Player.STATE_BUFFERING: - return PlaybackStateCompat.STATE_BUFFERING; - case Player.STATE_READY: - return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED; - case Player.STATE_ENDED: - return PlaybackStateCompat.STATE_PAUSED; - default: - return PlaybackStateCompat.STATE_NONE; - } - } - - private boolean canDispatchToPlaybackPreparer(long action) { - return playbackPreparer != null && (playbackPreparer.getSupportedPrepareActions() - & PlaybackPreparer.ACTIONS & action) != 0; - } - - private boolean canDispatchToRatingCallback(long action) { - return ratingCallback != null && (RatingCallback.ACTIONS & action) != 0; - } - - private boolean canDispatchToPlaybackController(long action) { - return (playbackController.getSupportedPlaybackActions(player) - & PlaybackController.ACTIONS & action) != 0; - } - - private boolean canDispatchToQueueNavigator(long action) { - return queueNavigator != null && (queueNavigator.getSupportedQueueNavigatorActions(player) - & QueueNavigator.ACTIONS & action) != 0; - } - - private class ExoPlayerEventListener extends Player.DefaultEventListener { + private class ExoPlayerEventListener implements Player.EventListener { private int currentWindowIndex; private int currentWindowCount; @Override - public void onTimelineChanged(Timeline timeline, Object manifest, - @Player.TimelineChangeReason int reason) { + public void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { int windowCount = player.getCurrentTimeline().getWindowCount(); int windowIndex = player.getCurrentWindowIndex(); if (queueNavigator != null) { queueNavigator.onTimelineChanged(player); - updateMediaSessionPlaybackState(); + invalidateMediaSessionPlaybackState(); } else if (currentWindowCount != windowCount || currentWindowIndex != windowIndex) { // active queue item and queue navigation actions may need to be updated - updateMediaSessionPlaybackState(); + invalidateMediaSessionPlaybackState(); } currentWindowCount = windowCount; currentWindowIndex = windowIndex; - updateMediaSessionMetadata(); + invalidateMediaSessionMetadata(); } @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - updateMediaSessionPlaybackState(); + invalidateMediaSessionPlaybackState(); } @Override public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { - mediaSession.setRepeatMode(repeatMode == Player.REPEAT_MODE_ONE - ? PlaybackStateCompat.REPEAT_MODE_ONE : repeatMode == Player.REPEAT_MODE_ALL - ? PlaybackStateCompat.REPEAT_MODE_ALL : PlaybackStateCompat.REPEAT_MODE_NONE); - updateMediaSessionPlaybackState(); + mediaSession.setRepeatMode( + repeatMode == Player.REPEAT_MODE_ONE + ? PlaybackStateCompat.REPEAT_MODE_ONE + : repeatMode == Player.REPEAT_MODE_ALL + ? PlaybackStateCompat.REPEAT_MODE_ALL + : PlaybackStateCompat.REPEAT_MODE_NONE); + invalidateMediaSessionPlaybackState(); } @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - mediaSession.setShuffleMode(shuffleModeEnabled ? PlaybackStateCompat.SHUFFLE_MODE_ALL - : PlaybackStateCompat.SHUFFLE_MODE_NONE); - updateMediaSessionPlaybackState(); + mediaSession.setShuffleMode( + shuffleModeEnabled + ? PlaybackStateCompat.SHUFFLE_MODE_ALL + : PlaybackStateCompat.SHUFFLE_MODE_NONE); + invalidateMediaSessionPlaybackState(); } @Override @@ -716,16 +821,19 @@ public final class MediaSessionConnector { queueNavigator.onCurrentWindowIndexChanged(player); } currentWindowIndex = player.getCurrentWindowIndex(); - updateMediaSessionMetadata(); + // Update playback state after queueNavigator.onCurrentWindowIndexChanged has been called + // and before updating metadata. + invalidateMediaSessionPlaybackState(); + invalidateMediaSessionMetadata(); + return; } - updateMediaSessionPlaybackState(); + invalidateMediaSessionPlaybackState(); } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - updateMediaSessionPlaybackState(); + invalidateMediaSessionPlaybackState(); } - } private class MediaSessionCallback extends MediaSessionCompat.Callback { @@ -812,7 +920,7 @@ public final class MediaSessionConnector { Map actionMap = customActionMap; if (actionMap.containsKey(action)) { actionMap.get(action).onCustomAction(action, extras); - updateMediaSessionPlaybackState(); + invalidateMediaSessionPlaybackState(); } } @@ -921,7 +1029,5 @@ public final class MediaSessionConnector { queueEditor.onRemoveQueueItemAt(player, index); } } - } - } diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java index 1db5889e00..057f59f62c 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 The Android Open Source Project + * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java index 853750077d..eadb320941 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 The Android Open Source Project + * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 26a7b6150a..6671add7e5 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -175,7 +175,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu private void publishFloatingQueueWindow(Player player) { if (player.getCurrentTimeline().isEmpty()) { - mediaSession.setQueue(Collections.emptyList()); + mediaSession.setQueue(Collections.emptyList()); activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; return; } diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 2b653c3f0e..4e6b11c495 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion @@ -28,7 +33,8 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'com.android.support:support-annotations:' + supportLibraryVersion - api 'com.squareup.okhttp3:okhttp:3.10.0' + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + api 'com.squareup.okhttp3:okhttp:3.11.0' } ext { diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index f2898005c1..1d0dfddb3f 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -15,24 +15,25 @@ */ package com.google.android.exoplayer2.ext.okhttp; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.net.Uri; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.upstream.BaseDataSource; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; -import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Predicate; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; +import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; import okhttp3.CacheControl; import okhttp3.Call; import okhttp3.HttpUrl; @@ -40,30 +41,28 @@ import okhttp3.MediaType; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; +import okhttp3.ResponseBody; -/** - * An {@link HttpDataSource} that delegates to Square's {@link Call.Factory}. - */ -public class OkHttpDataSource implements HttpDataSource { +/** An {@link HttpDataSource} that delegates to Square's {@link Call.Factory}. */ +public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { static { ExoPlayerLibraryInfo.registerModule("goog.exo.okhttp"); } - private static final AtomicReference skipBufferReference = new AtomicReference<>(); + private static final byte[] SKIP_BUFFER = new byte[4096]; - @NonNull private final Call.Factory callFactory; - @NonNull private final RequestProperties requestProperties; + private final Call.Factory callFactory; + private final RequestProperties requestProperties; - @Nullable private final String userAgent; - @Nullable private final Predicate contentTypePredicate; - @Nullable private final TransferListener listener; - @Nullable private final CacheControl cacheControl; - @Nullable private final RequestProperties defaultRequestProperties; + private final @Nullable String userAgent; + private final @Nullable Predicate contentTypePredicate; + private final @Nullable CacheControl cacheControl; + private final @Nullable RequestProperties defaultRequestProperties; - private DataSpec dataSpec; - private Response response; - private InputStream responseByteStream; + private @Nullable DataSpec dataSpec; + private @Nullable Response response; + private @Nullable InputStream responseByteStream; private boolean opened; private long bytesToSkip; @@ -77,11 +76,19 @@ public class OkHttpDataSource implements HttpDataSource { * by the source. * @param userAgent An optional User-Agent string. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}. + * predicate then a {@link InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. */ - public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent, + public OkHttpDataSource( + Call.Factory callFactory, + @Nullable String userAgent, @Nullable Predicate contentTypePredicate) { - this(callFactory, userAgent, contentTypePredicate, null); + this( + callFactory, + userAgent, + contentTypePredicate, + /* cacheControl= */ null, + /* defaultRequestProperties= */ null); } /** @@ -89,49 +96,35 @@ public class OkHttpDataSource implements HttpDataSource { * by the source. * @param userAgent An optional User-Agent string. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a {@link InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. - * @param listener An optional listener. - */ - public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent, - @Nullable Predicate contentTypePredicate, - @Nullable TransferListener listener) { - this(callFactory, userAgent, contentTypePredicate, listener, null, null); - } - - /** - * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use - * by the source. - * @param userAgent An optional User-Agent string. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a {@link InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. - * @param listener An optional listener. + * predicate then a {@link InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to - * the server as HTTP headers on every request. + * the server as HTTP headers on every request. */ - public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent, + public OkHttpDataSource( + Call.Factory callFactory, + @Nullable String userAgent, @Nullable Predicate contentTypePredicate, - @Nullable TransferListener listener, - @Nullable CacheControl cacheControl, @Nullable RequestProperties defaultRequestProperties) { + @Nullable CacheControl cacheControl, + @Nullable RequestProperties defaultRequestProperties) { + super(/* isNetwork= */ true); this.callFactory = Assertions.checkNotNull(callFactory); this.userAgent = userAgent; this.contentTypePredicate = contentTypePredicate; - this.listener = listener; this.cacheControl = cacheControl; this.defaultRequestProperties = defaultRequestProperties; this.requestProperties = new RequestProperties(); } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return response == null ? null : Uri.parse(response.request().url().toString()); } @Override public Map> getResponseHeaders() { - return response == null ? null : response.headers().toMultimap(); + return response == null ? Collections.emptyMap() : response.headers().toMultimap(); } @Override @@ -157,10 +150,16 @@ public class OkHttpDataSource implements HttpDataSource { this.dataSpec = dataSpec; this.bytesRead = 0; this.bytesSkipped = 0; + transferInitializing(dataSpec); + Request request = makeRequest(dataSpec); + Response response; + ResponseBody responseBody; try { - response = callFactory.newCall(request).execute(); - responseByteStream = response.body().byteStream(); + this.response = callFactory.newCall(request).execute(); + response = this.response; + responseBody = Assertions.checkNotNull(response.body()); + responseByteStream = responseBody.byteStream(); } catch (IOException e) { throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, dataSpec, HttpDataSourceException.TYPE_OPEN); @@ -181,8 +180,8 @@ public class OkHttpDataSource implements HttpDataSource { } // Check for a valid content type. - MediaType mediaType = response.body().contentType(); - String contentType = mediaType != null ? mediaType.toString() : null; + MediaType mediaType = responseBody.contentType(); + String contentType = mediaType != null ? mediaType.toString() : ""; if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) { closeConnectionQuietly(); throw new InvalidContentTypeException(contentType, dataSpec); @@ -197,14 +196,12 @@ public class OkHttpDataSource implements HttpDataSource { if (dataSpec.length != C.LENGTH_UNSET) { bytesToRead = dataSpec.length; } else { - long contentLength = response.body().contentLength(); + long contentLength = responseBody.contentLength(); bytesToRead = contentLength != -1 ? (contentLength - bytesToSkip) : C.LENGTH_UNSET; } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesToRead; } @@ -215,7 +212,8 @@ public class OkHttpDataSource implements HttpDataSource { skipInternal(); return readInternal(buffer, offset, readLength); } catch (IOException e) { - throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_READ); + throw new HttpDataSourceException( + e, Assertions.checkNotNull(dataSpec), HttpDataSourceException.TYPE_READ); } } @@ -223,9 +221,7 @@ public class OkHttpDataSource implements HttpDataSource { public void close() throws HttpDataSourceException { if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); closeConnectionQuietly(); } } @@ -262,15 +258,18 @@ public class OkHttpDataSource implements HttpDataSource { return bytesToRead == C.LENGTH_UNSET ? bytesToRead : bytesToRead - bytesRead; } - /** - * Establishes a connection. - */ - private Request makeRequest(DataSpec dataSpec) { + /** Establishes a connection. */ + private Request makeRequest(DataSpec dataSpec) throws HttpDataSourceException { long position = dataSpec.position; long length = dataSpec.length; boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP); HttpUrl url = HttpUrl.parse(dataSpec.uri.toString()); + if (url == null) { + throw new HttpDataSourceException( + "Malformed URL", dataSpec, HttpDataSourceException.TYPE_OPEN); + } + Request.Builder builder = new Request.Builder().url(url); if (cacheControl != null) { builder.cacheControl(cacheControl); @@ -297,9 +296,14 @@ public class OkHttpDataSource implements HttpDataSource { if (!allowGzip) { builder.addHeader("Accept-Encoding", "identity"); } - if (dataSpec.postBody != null) { - builder.post(RequestBody.create(null, dataSpec.postBody)); + RequestBody requestBody = null; + if (dataSpec.httpBody != null) { + requestBody = RequestBody.create(null, dataSpec.httpBody); + } else if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) { + // OkHttp requires a non-null body for POST requests. + requestBody = RequestBody.create(null, new byte[0]); } + builder.method(dataSpec.getHttpMethodString(), requestBody); return builder.build(); } @@ -316,15 +320,9 @@ public class OkHttpDataSource implements HttpDataSource { return; } - // Acquire the shared skip buffer. - byte[] skipBuffer = skipBufferReference.getAndSet(null); - if (skipBuffer == null) { - skipBuffer = new byte[4096]; - } - while (bytesSkipped != bytesToSkip) { - int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length); - int read = responseByteStream.read(skipBuffer, 0, readLength); + int readLength = (int) Math.min(bytesToSkip - bytesSkipped, SKIP_BUFFER.length); + int read = castNonNull(responseByteStream).read(SKIP_BUFFER, 0, readLength); if (Thread.currentThread().isInterrupted()) { throw new InterruptedIOException(); } @@ -332,13 +330,8 @@ public class OkHttpDataSource implements HttpDataSource { throw new EOFException(); } bytesSkipped += read; - if (listener != null) { - listener.onBytesTransferred(this, read); - } + bytesTransferred(read); } - - // Release the shared skip buffer. - skipBufferReference.set(skipBuffer); } /** @@ -367,7 +360,7 @@ public class OkHttpDataSource implements HttpDataSource { readLength = (int) Math.min(readLength, bytesRemaining); } - int read = responseByteStream.read(buffer, offset, readLength); + int read = castNonNull(responseByteStream).read(buffer, offset, readLength); if (read == -1) { if (bytesToRead != C.LENGTH_UNSET) { // End of stream reached having not read sufficient data. @@ -377,9 +370,7 @@ public class OkHttpDataSource implements HttpDataSource { } bytesRead += read; - if (listener != null) { - listener.onBytesTransferred(this, read); - } + bytesTransferred(read); return read; } @@ -387,8 +378,10 @@ public class OkHttpDataSource implements HttpDataSource { * Closes the current connection quietly, if there is one. */ private void closeConnectionQuietly() { - response.body().close(); - response = null; + if (response != null) { + Assertions.checkNotNull(response.body()).close(); + response = null; + } responseByteStream = null; } diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java index 32fc5a58cb..09f4e0b61a 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java @@ -15,9 +15,7 @@ */ package com.google.android.exoplayer2.ext.okhttp; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; @@ -30,10 +28,30 @@ import okhttp3.Call; */ public final class OkHttpDataSourceFactory extends BaseFactory { - @NonNull private final Call.Factory callFactory; - @Nullable private final String userAgent; - @Nullable private final TransferListener listener; - @Nullable private final CacheControl cacheControl; + private final Call.Factory callFactory; + private final @Nullable String userAgent; + private final @Nullable TransferListener listener; + private final @Nullable CacheControl cacheControl; + + /** + * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use + * by the sources created by the factory. + * @param userAgent An optional User-Agent string. + */ + public OkHttpDataSourceFactory(Call.Factory callFactory, @Nullable String userAgent) { + this(callFactory, userAgent, /* listener= */ null, /* cacheControl= */ null); + } + + /** + * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use + * by the sources created by the factory. + * @param userAgent An optional User-Agent string. + * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. + */ + public OkHttpDataSourceFactory( + Call.Factory callFactory, @Nullable String userAgent, @Nullable CacheControl cacheControl) { + this(callFactory, userAgent, /* listener= */ null, cacheControl); + } /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use @@ -41,9 +59,9 @@ public final class OkHttpDataSourceFactory extends BaseFactory { * @param userAgent An optional User-Agent string. * @param listener An optional listener. */ - public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent, - @Nullable TransferListener listener) { - this(callFactory, userAgent, listener, null); + public OkHttpDataSourceFactory( + Call.Factory callFactory, @Nullable String userAgent, @Nullable TransferListener listener) { + this(callFactory, userAgent, listener, /* cacheControl= */ null); } /** @@ -53,8 +71,10 @@ public final class OkHttpDataSourceFactory extends BaseFactory { * @param listener An optional listener. * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. */ - public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent, - @Nullable TransferListener listener, + public OkHttpDataSourceFactory( + Call.Factory callFactory, + @Nullable String userAgent, + @Nullable TransferListener listener, @Nullable CacheControl cacheControl) { this.callFactory = callFactory; this.userAgent = userAgent; @@ -65,8 +85,16 @@ public final class OkHttpDataSourceFactory extends BaseFactory { @Override protected OkHttpDataSource createDataSourceInternal( HttpDataSource.RequestProperties defaultRequestProperties) { - return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl, - defaultRequestProperties); + OkHttpDataSource dataSource = + new OkHttpDataSource( + callFactory, + userAgent, + /* contentTypePredicate= */ null, + cacheControl, + defaultRequestProperties); + if (listener != null) { + dataSource.addTransferListener(listener); + } + return dataSource; } - } diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index 2d20c65697..dc530d05aa 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java index c547cff434..8e3a213af1 100644 --- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java +++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java @@ -64,8 +64,7 @@ public class OpusPlaybackTest extends InstrumentationTestCase { } } - private static class TestPlaybackRunnable extends Player.DefaultEventListener - implements Runnable { + private static class TestPlaybackRunnable implements Player.EventListener, Runnable { private final Context context; private final Uri uri; diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index b94f3e9332..57937b4282 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -30,8 +30,10 @@ import com.google.android.exoplayer2.util.MimeTypes; */ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { + /** The number of input and output buffers. */ private static final int NUM_BUFFERS = 16; - private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6; + /** The default input buffer size. */ + private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6; private OpusDecoder decoder; @@ -88,8 +90,15 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { @Override protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) throws OpusDecoderException { - decoder = new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, - format.initializationData, mediaCrypto); + int initialInputBufferSize = + format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; + decoder = + new OpusDecoder( + NUM_BUFFERS, + NUM_BUFFERS, + initialInputBufferSize, + format.initializationData, + mediaCrypto); return decoder; } diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index c34e0b9999..2f2c65980a 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion 15 targetSdkVersion project.ext.targetSdkVersion diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java index 0601af4a2f..08c328ce81 100644 --- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java +++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java @@ -19,6 +19,7 @@ import android.net.Uri; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.upstream.BaseDataSource; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.TransferListener; @@ -26,40 +27,40 @@ import java.io.IOException; import net.butterflytv.rtmp_client.RtmpClient; import net.butterflytv.rtmp_client.RtmpClient.RtmpIOException; -/** - * A Real-Time Messaging Protocol (RTMP) {@link DataSource}. - */ -public final class RtmpDataSource implements DataSource { +/** A Real-Time Messaging Protocol (RTMP) {@link DataSource}. */ +public final class RtmpDataSource extends BaseDataSource { static { ExoPlayerLibraryInfo.registerModule("goog.exo.rtmp"); } - @Nullable private final TransferListener listener; - private RtmpClient rtmpClient; private Uri uri; public RtmpDataSource() { - this(null); + super(/* isNetwork= */ true); } /** * @param listener An optional listener. + * @deprecated Use {@link #RtmpDataSource()} and {@link #addTransferListener(TransferListener)}. */ - public RtmpDataSource(@Nullable TransferListener listener) { - this.listener = listener; + @Deprecated + public RtmpDataSource(@Nullable TransferListener listener) { + this(); + if (listener != null) { + addTransferListener(listener); + } } @Override public long open(DataSpec dataSpec) throws RtmpIOException { + transferInitializing(dataSpec); rtmpClient = new RtmpClient(); rtmpClient.open(dataSpec.uri.toString(), false); this.uri = dataSpec.uri; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return C.LENGTH_UNSET; } @@ -69,9 +70,7 @@ public final class RtmpDataSource implements DataSource { if (bytesRead == -1) { return C.RESULT_END_OF_INPUT; } - if (listener != null) { - listener.onBytesTransferred(this, bytesRead); - } + bytesTransferred(bytesRead); return bytesRead; } @@ -79,9 +78,7 @@ public final class RtmpDataSource implements DataSource { public void close() { if (uri != null) { uri = null; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } if (rtmpClient != null) { rtmpClient.close(); diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java index 0510e9c7da..3cf9b8de37 100644 --- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java +++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java @@ -25,17 +25,14 @@ import com.google.android.exoplayer2.upstream.TransferListener; */ public final class RtmpDataSourceFactory implements DataSource.Factory { - @Nullable - private final TransferListener listener; + private final @Nullable TransferListener listener; public RtmpDataSourceFactory() { this(null); } - /** - * @param listener An optional listener. - */ - public RtmpDataSourceFactory(@Nullable TransferListener listener) { + /** @param listener An optional listener. */ + public RtmpDataSourceFactory(@Nullable TransferListener listener) { this.listener = listener; } diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index 7dc95b388f..3fb627fd77 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index 09701f9542..bab7cb6fd7 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -95,8 +95,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase { } } - private static class TestPlaybackRunnable extends Player.DefaultEventListener - implements Runnable { + private static class TestPlaybackRunnable implements Player.EventListener, Runnable { private final Context context; private final Uri uri; diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index 7fde7678b8..08c413aba7 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -99,11 +99,8 @@ public class LibvpxVideoRenderer extends BaseRenderer { * requiring multiple output buffers to be dequeued at a time for it to make progress. */ private static final int NUM_OUTPUT_BUFFERS = 8; - /** - * The initial input buffer size. Input buffers are reallocated dynamically if this value is - * insufficient. - */ - private static final int INITIAL_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp. + /** The default input buffer size. */ + private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp. private final boolean scaleToFit; private final boolean disableLoopFilter; @@ -114,6 +111,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { private final FormatHolder formatHolder; private final DecoderInputBuffer flagsOnlyBuffer; private final DrmSessionManager drmSessionManager; + private final boolean useSurfaceYuvOutput; private Format format; private VpxDecoder decoder; @@ -177,7 +175,8 @@ public class LibvpxVideoRenderer extends BaseRenderer { maxDroppedFramesToNotify, /* drmSessionManager= */ null, /* playClearSamplesWithoutKeys= */ false, - /* disableLoopFilter= */ false); + /* disableLoopFilter= */ false, + /* useSurfaceYuvOutput= */ false); } /** @@ -197,11 +196,18 @@ public class LibvpxVideoRenderer extends BaseRenderer { * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. * @param disableLoopFilter Disable the libvpx in-loop smoothing filter. + * @param useSurfaceYuvOutput Directly output YUV to the Surface via ANativeWindow. */ - public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs, - Handler eventHandler, VideoRendererEventListener eventListener, - int maxDroppedFramesToNotify, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, boolean disableLoopFilter) { + public LibvpxVideoRenderer( + boolean scaleToFit, + long allowedJoiningTimeMs, + Handler eventHandler, + VideoRendererEventListener eventListener, + int maxDroppedFramesToNotify, + DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + boolean disableLoopFilter, + boolean useSurfaceYuvOutput) { super(C.TRACK_TYPE_VIDEO); this.scaleToFit = scaleToFit; this.disableLoopFilter = disableLoopFilter; @@ -209,6 +215,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; this.drmSessionManager = drmSessionManager; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + this.useSurfaceYuvOutput = useSurfaceYuvOutput; joiningDeadlineMs = C.TIME_UNSET; clearReportedVideoSize(); formatHolder = new FormatHolder(); @@ -549,21 +556,25 @@ public class LibvpxVideoRenderer extends BaseRenderer { * * @param outputBuffer The buffer to render. */ - protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) { + protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) throws VpxDecoderException { int bufferMode = outputBuffer.mode; boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null; + boolean renderSurface = bufferMode == VpxDecoder.OUTPUT_MODE_SURFACE_YUV && surface != null; boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null; lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; - if (!renderRgb && !renderYuv) { + if (!renderRgb && !renderYuv && !renderSurface) { dropOutputBuffer(outputBuffer); } else { maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height); if (renderRgb) { renderRgbFrame(outputBuffer, scaleToFit); outputBuffer.release(); - } else /* renderYuv */ { + } else if (renderYuv) { outputBufferRenderer.setOutputBuffer(outputBuffer); // The renderer will release the buffer. + } else { // renderSurface + decoder.renderToSurface(outputBuffer, surface); + outputBuffer.release(); } consecutiveDroppedFrameCount = 0; decoderCounters.renderedOutputBufferCount++; @@ -633,8 +644,13 @@ public class LibvpxVideoRenderer extends BaseRenderer { // The output has changed. this.surface = surface; this.outputBufferRenderer = outputBufferRenderer; - outputMode = outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV - : surface != null ? VpxDecoder.OUTPUT_MODE_RGB : VpxDecoder.OUTPUT_MODE_NONE; + if (surface != null) { + outputMode = + useSurfaceYuvOutput ? VpxDecoder.OUTPUT_MODE_SURFACE_YUV : VpxDecoder.OUTPUT_MODE_RGB; + } else { + outputMode = + outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV : VpxDecoder.OUTPUT_MODE_NONE; + } if (outputMode != VpxDecoder.OUTPUT_MODE_NONE) { if (decoder != null) { decoder.setOutputMode(outputMode); @@ -684,13 +700,16 @@ public class LibvpxVideoRenderer extends BaseRenderer { try { long decoderInitializingTimestamp = SystemClock.elapsedRealtime(); TraceUtil.beginSection("createVpxDecoder"); + int initialInputBufferSize = + format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; decoder = new VpxDecoder( NUM_INPUT_BUFFERS, NUM_OUTPUT_BUFFERS, - INITIAL_INPUT_BUFFER_SIZE, + initialInputBufferSize, mediaCrypto, - disableLoopFilter); + disableLoopFilter, + useSurfaceYuvOutput); decoder.setOutputMode(outputMode); TraceUtil.endSection(); long decoderInitializedTimestamp = SystemClock.elapsedRealtime(); @@ -817,7 +836,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { * @throws ExoPlaybackException If an error occurs processing the output buffer. */ private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs) - throws ExoPlaybackException { + throws ExoPlaybackException, VpxDecoderException { if (initialPositionUs == C.TIME_UNSET) { initialPositionUs = positionUs; } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 6f8c0a1918..51ef8e9bcf 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.vp9; +import android.view.Surface; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.SimpleDecoder; @@ -31,6 +32,7 @@ import java.nio.ByteBuffer; public static final int OUTPUT_MODE_NONE = -1; public static final int OUTPUT_MODE_YUV = 0; public static final int OUTPUT_MODE_RGB = 1; + public static final int OUTPUT_MODE_SURFACE_YUV = 2; private static final int NO_ERROR = 0; private static final int DECODE_ERROR = 1; @@ -50,10 +52,17 @@ import java.nio.ByteBuffer; * @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted * content. Maybe null and can be ignored if decoder does not handle encrypted content. * @param disableLoopFilter Disable the libvpx in-loop smoothing filter. + * @param enableSurfaceYuvOutputMode Whether OUTPUT_MODE_SURFACE_YUV is allowed. * @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder. */ - public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, - ExoMediaCrypto exoMediaCrypto, boolean disableLoopFilter) throws VpxDecoderException { + public VpxDecoder( + int numInputBuffers, + int numOutputBuffers, + int initialInputBufferSize, + ExoMediaCrypto exoMediaCrypto, + boolean disableLoopFilter, + boolean enableSurfaceYuvOutputMode) + throws VpxDecoderException { super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]); if (!VpxLibrary.isAvailable()) { throw new VpxDecoderException("Failed to load decoder native libraries."); @@ -62,7 +71,7 @@ import java.nio.ByteBuffer; if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) { throw new VpxDecoderException("Vpx decoder does not support secure decode."); } - vpxDecContext = vpxInit(disableLoopFilter); + vpxDecContext = vpxInit(disableLoopFilter, enableSurfaceYuvOutputMode); if (vpxDecContext == 0) { throw new VpxDecoderException("Failed to initialize decoder"); } @@ -96,6 +105,11 @@ import java.nio.ByteBuffer; @Override protected void releaseOutputBuffer(VpxOutputBuffer buffer) { + // Decode only frames do not acquire a reference on the internal decoder buffer and thus do not + // require a call to vpxReleaseFrame. + if (outputMode == OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) { + vpxReleaseFrame(vpxDecContext, buffer); + } super.releaseOutputBuffer(buffer); } @@ -145,13 +159,36 @@ import java.nio.ByteBuffer; vpxClose(vpxDecContext); } - private native long vpxInit(boolean disableLoopFilter); + /** Renders the outputBuffer to the surface. Used with OUTPUT_MODE_SURFACE_YUV only. */ + public void renderToSurface(VpxOutputBuffer outputBuffer, Surface surface) + throws VpxDecoderException { + int getFrameResult = vpxRenderFrame(vpxDecContext, surface, outputBuffer); + if (getFrameResult == -1) { + throw new VpxDecoderException("Buffer render failed."); + } + } + + private native long vpxInit(boolean disableLoopFilter, boolean enableSurfaceYuvOutputMode); + private native long vpxClose(long context); private native long vpxDecode(long context, ByteBuffer encoded, int length); private native long vpxSecureDecode(long context, ByteBuffer encoded, int length, ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv, int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData); private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer); + + /** + * Renders the frame to the surface. Used with OUTPUT_MODE_SURFACE_YUV only. Must only be called + * if {@link #vpxInit} was called with {@code enableBufferManager = true}. + */ + private native int vpxRenderFrame(long context, Surface surface, VpxOutputBuffer outputBuffer); + + /** + * Releases the frame. Used with OUTPUT_MODE_SURFACE_YUV only. Must only be called if {@link + * #vpxInit} was called with {@code enableBufferManager = true}. + */ + private native int vpxReleaseFrame(long context, VpxOutputBuffer outputBuffer); + private native int vpxGetErrorCode(long context); private native String vpxGetErrorMessage(long context); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java index 2618bf7c62..fa0df1cfa9 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java @@ -30,6 +30,8 @@ import java.nio.ByteBuffer; public static final int COLORSPACE_BT2020 = 3; private final VpxDecoder owner; + /** Decoder private data. */ + public int decoderPrivate; public int mode; /** diff --git a/extensions/vp9/src/main/jni/Android.mk b/extensions/vp9/src/main/jni/Android.mk index 92fed0a064..868b869d56 100644 --- a/extensions/vp9/src/main/jni/Android.mk +++ b/extensions/vp9/src/main/jni/Android.mk @@ -35,7 +35,7 @@ LOCAL_MODULE := libvpxJNI LOCAL_ARM_MODE := arm LOCAL_CPP_EXTENSION := .cc LOCAL_SRC_FILES := vpx_jni.cc -LOCAL_LDLIBS := -llog -lz -lm +LOCAL_LDLIBS := -llog -lz -lm -landroid LOCAL_SHARED_LIBRARIES := libvpx LOCAL_STATIC_LIBRARIES := libyuv_static cpufeatures include $(BUILD_SHARED_LIBRARY) diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index 12bc30112d..f36c433b22 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -21,7 +21,9 @@ #include #include - +#include +#include +#include #include #include #include @@ -63,6 +65,11 @@ static jmethodID initForRgbFrame; static jmethodID initForYuvFrame; static jfieldID dataField; static jfieldID outputModeField; +static jfieldID decoderPrivateField; + +// android.graphics.ImageFormat.YV12. +static const int kHalPixelFormatYV12 = 0x32315659; +static const int kDecoderPrivateBase = 0x100; static int errorCode; jint JNI_OnLoad(JavaVM* vm, void* reserved) { @@ -282,13 +289,166 @@ static void convert_16_to_8_standard(const vpx_image_t* const img, } } -DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) { - vpx_codec_ctx_t* context = new vpx_codec_ctx_t(); +struct JniFrameBuffer { + friend class JniBufferManager; + + int stride[4]; + uint8_t* planes[4]; + int d_w; + int d_h; + + private: + int id; + int ref_count; + vpx_codec_frame_buffer_t vpx_fb; +}; + +class JniBufferManager { + static const int MAX_FRAMES = 32; + + JniFrameBuffer* all_buffers[MAX_FRAMES]; + int all_buffer_count = 0; + + JniFrameBuffer* free_buffers[MAX_FRAMES]; + int free_buffer_count = 0; + + pthread_mutex_t mutex; + + public: + JniBufferManager() { pthread_mutex_init(&mutex, NULL); } + + ~JniBufferManager() { + while (all_buffer_count--) { + free(all_buffers[all_buffer_count]->vpx_fb.data); + } + } + + int get_buffer(size_t min_size, vpx_codec_frame_buffer_t* fb) { + pthread_mutex_lock(&mutex); + JniFrameBuffer* out_buffer; + if (free_buffer_count) { + out_buffer = free_buffers[--free_buffer_count]; + if (out_buffer->vpx_fb.size < min_size) { + free(out_buffer->vpx_fb.data); + out_buffer->vpx_fb.data = (uint8_t*)malloc(min_size); + out_buffer->vpx_fb.size = min_size; + } + } else { + out_buffer = new JniFrameBuffer(); + out_buffer->id = all_buffer_count; + all_buffers[all_buffer_count++] = out_buffer; + out_buffer->vpx_fb.data = (uint8_t*)malloc(min_size); + out_buffer->vpx_fb.size = min_size; + out_buffer->vpx_fb.priv = &out_buffer->id; + } + *fb = out_buffer->vpx_fb; + int retVal = 0; + if (!out_buffer->vpx_fb.data || all_buffer_count >= MAX_FRAMES) { + LOGE("ERROR: JniBufferManager get_buffer OOM."); + retVal = -1; + } else { + memset(fb->data, 0, fb->size); + } + out_buffer->ref_count = 1; + pthread_mutex_unlock(&mutex); + return retVal; + } + + JniFrameBuffer* get_buffer(int id) const { + if (id < 0 || id >= all_buffer_count) { + LOGE("ERROR: JniBufferManager get_buffer invalid id %d.", id); + return NULL; + } + return all_buffers[id]; + } + + void add_ref(int id) { + if (id < 0 || id >= all_buffer_count) { + LOGE("ERROR: JniBufferManager add_ref invalid id %d.", id); + return; + } + pthread_mutex_lock(&mutex); + all_buffers[id]->ref_count++; + pthread_mutex_unlock(&mutex); + } + + int release(int id) { + if (id < 0 || id >= all_buffer_count) { + LOGE("ERROR: JniBufferManager release invalid id %d.", id); + return -1; + } + pthread_mutex_lock(&mutex); + JniFrameBuffer* buffer = all_buffers[id]; + if (!buffer->ref_count) { + LOGE("ERROR: JniBufferManager release, buffer already released."); + pthread_mutex_unlock(&mutex); + return -1; + } + if (!--buffer->ref_count) { + free_buffers[free_buffer_count++] = buffer; + } + pthread_mutex_unlock(&mutex); + return 0; + } +}; + +struct JniCtx { + JniCtx(bool enableBufferManager) { + if (enableBufferManager) { + buffer_manager = new JniBufferManager(); + } + } + + ~JniCtx() { + if (native_window) { + ANativeWindow_release(native_window); + } + if (buffer_manager) { + delete buffer_manager; + } + } + + void acquire_native_window(JNIEnv* env, jobject new_surface) { + if (surface != new_surface) { + if (native_window) { + ANativeWindow_release(native_window); + } + native_window = ANativeWindow_fromSurface(env, new_surface); + surface = new_surface; + width = 0; + } + } + + JniBufferManager* buffer_manager = NULL; + vpx_codec_ctx_t* decoder = NULL; + ANativeWindow* native_window = NULL; + jobject surface = NULL; + int width = 0; + int height = 0; +}; + +int vpx_get_frame_buffer(void* priv, size_t min_size, + vpx_codec_frame_buffer_t* fb) { + JniBufferManager* const buffer_manager = + reinterpret_cast(priv); + return buffer_manager->get_buffer(min_size, fb); +} + +int vpx_release_frame_buffer(void* priv, vpx_codec_frame_buffer_t* fb) { + JniBufferManager* const buffer_manager = + reinterpret_cast(priv); + return buffer_manager->release(*(int*)fb->priv); +} + +DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter, + jboolean enableBufferManager) { + JniCtx* context = new JniCtx(enableBufferManager); + context->decoder = new vpx_codec_ctx_t(); vpx_codec_dec_cfg_t cfg = {0, 0, 0}; cfg.threads = android_getCpuCount(); errorCode = 0; - vpx_codec_err_t err = vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo, - &cfg, 0); + vpx_codec_err_t err = + vpx_codec_dec_init(context->decoder, &vpx_codec_vp9_dx_algo, &cfg, 0); if (err) { LOGE("ERROR: Failed to initialize libvpx decoder, error = %d.", err); errorCode = err; @@ -296,11 +456,20 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) { } if (disableLoopFilter) { // TODO(b/71930387): Use vpx_codec_control(), not vpx_codec_control_(). - err = vpx_codec_control_(context, VP9_SET_SKIP_LOOP_FILTER, true); + err = vpx_codec_control_(context->decoder, VP9_SET_SKIP_LOOP_FILTER, true); if (err) { LOGE("ERROR: Failed to shut off libvpx loop filter, error = %d.", err); } } + if (enableBufferManager) { + err = vpx_codec_set_frame_buffer_functions( + context->decoder, vpx_get_frame_buffer, vpx_release_frame_buffer, + context->buffer_manager); + if (err) { + LOGE("ERROR: Failed to set libvpx frame buffer functions, error = %d.", + err); + } + } // Populate JNI References. const jclass outputBufferClass = env->FindClass( @@ -312,16 +481,17 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) { dataField = env->GetFieldID(outputBufferClass, "data", "Ljava/nio/ByteBuffer;"); outputModeField = env->GetFieldID(outputBufferClass, "mode", "I"); - + decoderPrivateField = + env->GetFieldID(outputBufferClass, "decoderPrivate", "I"); return reinterpret_cast(context); } DECODER_FUNC(jlong, vpxDecode, jlong jContext, jobject encoded, jint len) { - vpx_codec_ctx_t* const context = reinterpret_cast(jContext); + JniCtx* const context = reinterpret_cast(jContext); const uint8_t* const buffer = reinterpret_cast(env->GetDirectBufferAddress(encoded)); const vpx_codec_err_t status = - vpx_codec_decode(context, buffer, len, NULL, 0); + vpx_codec_decode(context->decoder, buffer, len, NULL, 0); errorCode = 0; if (status != VPX_CODEC_OK) { LOGE("ERROR: vpx_codec_decode() failed, status= %d", status); @@ -343,16 +513,16 @@ DECODER_FUNC(jlong, vpxSecureDecode, jlong jContext, jobject encoded, jint len, } DECODER_FUNC(jlong, vpxClose, jlong jContext) { - vpx_codec_ctx_t* const context = reinterpret_cast(jContext); - vpx_codec_destroy(context); + JniCtx* const context = reinterpret_cast(jContext); + vpx_codec_destroy(context->decoder); delete context; return 0; } DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { - vpx_codec_ctx_t* const context = reinterpret_cast(jContext); + JniCtx* const context = reinterpret_cast(jContext); vpx_codec_iter_t iter = NULL; - const vpx_image_t* const img = vpx_codec_get_frame(context, &iter); + const vpx_image_t* const img = vpx_codec_get_frame(context->decoder, &iter); if (img == NULL) { return 1; @@ -360,6 +530,7 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { const int kOutputModeYuv = 0; const int kOutputModeRgb = 1; + const int kOutputModeSurfaceYuv = 2; int outputMode = env->GetIntField(jOutputBuffer, outputModeField); if (outputMode == kOutputModeRgb) { @@ -435,13 +606,93 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { memcpy(data + yLength, img->planes[VPX_PLANE_U], uvLength); memcpy(data + yLength + uvLength, img->planes[VPX_PLANE_V], uvLength); } + } else if (outputMode == kOutputModeSurfaceYuv && + img->fmt != VPX_IMG_FMT_I42016) { + if (!context->buffer_manager) { + return -1; // enableBufferManager was not set in vpxInit. + } + int id = *(int*)img->fb_priv; + context->buffer_manager->add_ref(id); + JniFrameBuffer* jfb = context->buffer_manager->get_buffer(id); + for (int i = 2; i >= 0; i--) { + jfb->stride[i] = img->stride[i]; + jfb->planes[i] = (uint8_t*)img->planes[i]; + } + jfb->d_w = img->d_w; + jfb->d_h = img->d_h; + env->SetIntField(jOutputBuffer, decoderPrivateField, + id + kDecoderPrivateBase); } return 0; } +DECODER_FUNC(jint, vpxRenderFrame, jlong jContext, jobject jSurface, + jobject jOutputBuffer) { + JniCtx* const context = reinterpret_cast(jContext); + const int id = env->GetIntField(jOutputBuffer, decoderPrivateField) - + kDecoderPrivateBase; + JniFrameBuffer* srcBuffer = context->buffer_manager->get_buffer(id); + context->acquire_native_window(env, jSurface); + if (context->native_window == NULL || !srcBuffer) { + return 1; + } + if (context->width != srcBuffer->d_w || context->height != srcBuffer->d_h) { + ANativeWindow_setBuffersGeometry(context->native_window, srcBuffer->d_w, + srcBuffer->d_h, kHalPixelFormatYV12); + context->width = srcBuffer->d_w; + context->height = srcBuffer->d_h; + } + ANativeWindow_Buffer buffer; + int result = ANativeWindow_lock(context->native_window, &buffer, NULL); + if (buffer.bits == NULL || result) { + return -1; + } + // Y + const size_t src_y_stride = srcBuffer->stride[VPX_PLANE_Y]; + int stride = srcBuffer->d_w; + const uint8_t* src_base = + reinterpret_cast(srcBuffer->planes[VPX_PLANE_Y]); + uint8_t* dest_base = (uint8_t*)buffer.bits; + for (int y = 0; y < srcBuffer->d_h; y++) { + memcpy(dest_base, src_base, stride); + src_base += src_y_stride; + dest_base += buffer.stride; + } + // UV + const int src_uv_stride = srcBuffer->stride[VPX_PLANE_U]; + const int dest_uv_stride = (buffer.stride / 2 + 15) & (~15); + const int32_t buffer_uv_height = (buffer.height + 1) / 2; + const int32_t height = + std::min((int32_t)(srcBuffer->d_h + 1) / 2, buffer_uv_height); + stride = (srcBuffer->d_w + 1) / 2; + src_base = reinterpret_cast(srcBuffer->planes[VPX_PLANE_U]); + const uint8_t* src_v_base = + reinterpret_cast(srcBuffer->planes[VPX_PLANE_V]); + uint8_t* dest_v_base = + ((uint8_t*)buffer.bits) + buffer.stride * buffer.height; + dest_base = dest_v_base + buffer_uv_height * dest_uv_stride; + for (int y = 0; y < height; y++) { + memcpy(dest_base, src_base, stride); + memcpy(dest_v_base, src_v_base, stride); + src_base += src_uv_stride; + src_v_base += src_uv_stride; + dest_base += dest_uv_stride; + dest_v_base += dest_uv_stride; + } + return ANativeWindow_unlockAndPost(context->native_window); +} + +DECODER_FUNC(void, vpxReleaseFrame, jlong jContext, jobject jOutputBuffer) { + JniCtx* const context = reinterpret_cast(jContext); + const int id = env->GetIntField(jOutputBuffer, decoderPrivateField) - + kDecoderPrivateBase; + env->SetIntField(jOutputBuffer, decoderPrivateField, -1); + context->buffer_manager->release(id); +} + DECODER_FUNC(jstring, vpxGetErrorMessage, jlong jContext) { - vpx_codec_ctx_t* const context = reinterpret_cast(jContext); - return env->NewStringUTF(vpx_codec_error(context)); + JniCtx* const context = reinterpret_cast(jContext); + return env->NewStringUTF(vpx_codec_error(context->decoder)); } DECODER_FUNC(jint, vpxGetErrorCode, jlong jContext) { return errorCode; } diff --git a/library/core/build.gradle b/library/core/build.gradle index d2fa5e25f8..947972392f 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -18,10 +18,22 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + // The following argument makes the Android Test Orchestrator run its + // "pm clear" command after each test invocation. This command ensures + // that the app's state is completely cleared between tests. + testInstrumentationRunnerArguments clearPackageData: 'true' } // Workaround to prevent circular dependency on project :testutils. @@ -47,10 +59,14 @@ android { dependencies { implementation 'com.android.support:support-annotations:' + supportLibraryVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion + androidTestImplementation 'androidx.test:runner:' + testRunnerVersion + androidTestImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion + androidTestAnnotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion testImplementation 'com.google.truth:truth:' + truthVersion testImplementation 'junit:junit:' + junitVersion testImplementation 'org.mockito:mockito-core:' + mockitoVersion diff --git a/library/core/src/androidTest/assets/bitmap/image_256_256.png b/library/core/src/androidTest/assets/bitmap/image_256_256.png new file mode 100644 index 0000000000..cf1403eebd Binary files /dev/null and b/library/core/src/androidTest/assets/bitmap/image_256_256.png differ diff --git a/library/core/src/androidTest/assets/bitmap/image_80_60.bmp b/library/core/src/androidTest/assets/bitmap/image_80_60.bmp new file mode 100644 index 0000000000..440c80f1b5 Binary files /dev/null and b/library/core/src/androidTest/assets/bitmap/image_80_60.bmp differ diff --git a/library/core/src/androidTest/assets/mp4/testvid_1022ms.mp4 b/library/core/src/androidTest/assets/mp4/testvid_1022ms.mp4 new file mode 100644 index 0000000000..bbd2729c4d Binary files /dev/null and b/library/core/src/androidTest/assets/mp4/testvid_1022ms.mp4 differ diff --git a/library/core/src/androidTest/assets/mp4/video000.png b/library/core/src/androidTest/assets/mp4/video000.png new file mode 100644 index 0000000000..5f2758fb29 Binary files /dev/null and b/library/core/src/androidTest/assets/mp4/video000.png differ diff --git a/library/core/src/androidTest/assets/mp4/video014.png b/library/core/src/androidTest/assets/mp4/video014.png new file mode 100644 index 0000000000..35bf00dcfa Binary files /dev/null and b/library/core/src/androidTest/assets/mp4/video014.png differ diff --git a/library/core/src/androidTest/assets/mp4/video015.png b/library/core/src/androidTest/assets/mp4/video015.png new file mode 100644 index 0000000000..a6dfa8ce2b Binary files /dev/null and b/library/core/src/androidTest/assets/mp4/video015.png differ diff --git a/library/core/src/androidTest/assets/mp4/video016.png b/library/core/src/androidTest/assets/mp4/video016.png new file mode 100644 index 0000000000..5877573d71 Binary files /dev/null and b/library/core/src/androidTest/assets/mp4/video016.png differ diff --git a/library/core/src/androidTest/assets/mp4/video029.png b/library/core/src/androidTest/assets/mp4/video029.png new file mode 100644 index 0000000000..9ab47773d4 Binary files /dev/null and b/library/core/src/androidTest/assets/mp4/video029.png differ diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java index 3465393853..49329c38c0 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.upstream; import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.fail; -import android.app.Instrumentation; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; @@ -28,48 +28,58 @@ import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.test.InstrumentationTestCase; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; -/** - * Unit tests for {@link ContentDataSource}. - */ -public final class ContentDataSourceTest extends InstrumentationTestCase { +/** Unit tests for {@link ContentDataSource}. */ +@RunWith(AndroidJUnit4.class) +public final class ContentDataSourceTest { private static final String AUTHORITY = "com.google.android.exoplayer2.core.test"; private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; + @Test public void testRead() throws Exception { - assertData(getInstrumentation(), 0, C.LENGTH_UNSET, false); + assertData(0, C.LENGTH_UNSET, false); } + @Test public void testReadPipeMode() throws Exception { - assertData(getInstrumentation(), 0, C.LENGTH_UNSET, true); + assertData(0, C.LENGTH_UNSET, true); } + @Test public void testReadFixedLength() throws Exception { - assertData(getInstrumentation(), 0, 100, false); + assertData(0, 100, false); } + @Test public void testReadFromOffsetToEndOfInput() throws Exception { - assertData(getInstrumentation(), 1, C.LENGTH_UNSET, false); + assertData(1, C.LENGTH_UNSET, false); } + @Test public void testReadFromOffsetToEndOfInputPipeMode() throws Exception { - assertData(getInstrumentation(), 1, C.LENGTH_UNSET, true); + assertData(1, C.LENGTH_UNSET, true); } + @Test public void testReadFromOffsetFixedLength() throws Exception { - assertData(getInstrumentation(), 1, 100, false); + assertData(1, 100, false); } + @Test public void testReadInvalidUri() throws Exception { - ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); + ContentDataSource dataSource = + new ContentDataSource(InstrumentationRegistry.getTargetContext()); Uri contentUri = TestContentProvider.buildUri("does/not.exist", false); DataSpec dataSpec = new DataSpec(contentUri); try { @@ -83,13 +93,14 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { } } - private static void assertData(Instrumentation instrumentation, int offset, int length, - boolean pipeMode) throws IOException { + private static void assertData(int offset, int length, boolean pipeMode) throws IOException { Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode); - ContentDataSource dataSource = new ContentDataSource(instrumentation.getContext()); + ContentDataSource dataSource = + new ContentDataSource(InstrumentationRegistry.getTargetContext()); try { DataSpec dataSpec = new DataSpec(contentUri, offset, length, null); - byte[] completeData = TestUtil.getByteArray(instrumentation.getContext(), DATA_PATH); + byte[] completeData = + TestUtil.getByteArray(InstrumentationRegistry.getTargetContext(), DATA_PATH); byte[] expectedData = Arrays.copyOfRange(completeData, offset, length == C.LENGTH_UNSET ? completeData.length : offset + length); TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index 58531346ab..964f7266b5 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -19,8 +19,9 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import android.net.Uri; -import android.test.InstrumentationTestCase; import android.util.SparseArray; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; import java.io.File; @@ -29,9 +30,14 @@ import java.io.FileOutputStream; import java.io.IOException; import java.util.Collection; import java.util.Set; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; /** Tests {@link CachedContentIndex}. */ -public class CachedContentIndexTest extends InstrumentationTestCase { +@RunWith(AndroidJUnit4.class) +public class CachedContentIndexTest { private final byte[] testIndexV1File = { 0, 0, 0, 1, // version @@ -70,19 +76,19 @@ public class CachedContentIndexTest extends InstrumentationTestCase { private CachedContentIndex index; private File cacheDir; - @Override + @Before public void setUp() throws Exception { - super.setUp(); - cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + cacheDir = + Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() { Util.recursiveDelete(cacheDir); - super.tearDown(); } + @Test public void testAddGetRemove() throws Exception { final String key1 = "key1"; final String key2 = "key2"; @@ -132,10 +138,12 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(cacheSpanFile.exists()).isTrue(); } + @Test public void testStoreAndLoad() throws Exception { assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir)); } + @Test public void testLoadV1() throws Exception { FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); fos.write(testIndexV1File); @@ -153,6 +161,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560); } + @Test public void testLoadV2() throws Exception { FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); fos.write(testIndexV2File); @@ -171,7 +180,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560); } - public void testAssignIdForKeyAndGetKeyForId() throws Exception { + @Test + public void testAssignIdForKeyAndGetKeyForId() { final String key1 = "key1"; final String key2 = "key2"; int id1 = index.assignIdForKey(key1); @@ -183,7 +193,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(index.assignIdForKey(key2)).isEqualTo(id2); } - public void testGetNewId() throws Exception { + @Test + public void testGetNewId() { SparseArray idToKey = new SparseArray<>(); assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0); idToKey.put(10, ""); @@ -194,6 +205,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(1); } + @Test public void testEncryption() throws Exception { byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key @@ -250,7 +262,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key)); } - public void testRemoveEmptyNotLockedCachedContent() throws Exception { + @Test + public void testRemoveEmptyNotLockedCachedContent() { CachedContent cachedContent = index.getOrAdd("key1"); index.maybeRemove(cachedContent.key); @@ -258,6 +271,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(index.get(cachedContent.key)).isNull(); } + @Test public void testCantRemoveNotEmptyCachedContent() throws Exception { CachedContent cachedContent = index.getOrAdd("key1"); File cacheSpanFile = @@ -270,7 +284,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(index.get(cachedContent.key)).isNotNull(); } - public void testCantRemoveLockedCachedContent() throws Exception { + @Test + public void testCantRemoveLockedCachedContent() { CachedContent cachedContent = index.getOrAdd("key1"); cachedContent.setLocked(true); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java index 637a19cdd2..c422bf33fa 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java @@ -18,7 +18,8 @@ package com.google.android.exoplayer2.upstream.cache; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import android.test.InstrumentationTestCase; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.FileOutputStream; @@ -26,11 +27,14 @@ import java.io.IOException; import java.util.HashMap; import java.util.Set; import java.util.TreeSet; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; -/** - * Unit tests for {@link SimpleCacheSpan}. - */ -public class SimpleCacheSpanTest extends InstrumentationTestCase { +/** Unit tests for {@link SimpleCacheSpan}. */ +@RunWith(AndroidJUnit4.class) +public class SimpleCacheSpanTest { private CachedContentIndex index; private File cacheDir; @@ -49,19 +53,19 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase { return SimpleCacheSpan.createCacheEntry(cacheFile, index); } - @Override - protected void setUp() throws Exception { - super.setUp(); - cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + @Before + public void setUp() throws Exception { + cacheDir = + Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() { Util.recursiveDelete(cacheDir); - super.tearDown(); } + @Test public void testCacheFile() throws Exception { assertCacheSpan("key1", 0, 0); assertCacheSpan("key2", 1, 2); @@ -80,6 +84,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase { + "A paragraph-separator character \u2029", 1, 2); } + @Test public void testUpgradeFileName() throws Exception { String key = "asd\u00aa"; int id = index.assignIdForKey(key); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index de210f5eff..87499a9cb1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -24,6 +24,7 @@ import android.media.MediaFormat; import android.support.annotation.IntDef; import android.view.Surface; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.audio.AuxEffectInfo; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -77,6 +78,12 @@ public final class C { */ public static final long NANOS_PER_SECOND = 1000000000L; + /** The number of bits per byte. */ + public static final int BITS_PER_BYTE = 8; + + /** The number of bytes per float. */ + public static final int BYTES_PER_FLOAT = 4; + /** * The name of the ASCII charset. */ @@ -136,6 +143,8 @@ public final class C { ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, + ENCODING_PCM_MU_LAW, + ENCODING_PCM_A_LAW, ENCODING_AC3, ENCODING_E_AC3, ENCODING_DTS, @@ -144,12 +153,19 @@ public final class C { }) public @interface Encoding {} - /** - * Represents a PCM audio encoding, or an invalid or unset value. - */ + /** Represents a PCM audio encoding, or an invalid or unset value. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, - ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT}) + @IntDef({ + Format.NO_VALUE, + ENCODING_INVALID, + ENCODING_PCM_8BIT, + ENCODING_PCM_16BIT, + ENCODING_PCM_24BIT, + ENCODING_PCM_32BIT, + ENCODING_PCM_FLOAT, + ENCODING_PCM_MU_LAW, + ENCODING_PCM_A_LAW + }) public @interface PcmEncoding {} /** @see AudioFormat#ENCODING_INVALID */ public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID; @@ -163,6 +179,10 @@ public final class C { public static final int ENCODING_PCM_32BIT = 0x40000000; /** @see AudioFormat#ENCODING_PCM_FLOAT */ public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT; + /** Audio encoding for mu-law. */ + public static final int ENCODING_PCM_MU_LAW = 0x10000000; + /** Audio encoding for A-law. */ + public static final int ENCODING_PCM_A_LAW = 0x20000000; /** @see AudioFormat#ENCODING_AC3 */ public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; /** @see AudioFormat#ENCODING_E_AC3 */ @@ -174,13 +194,6 @@ public final class C { /** @see AudioFormat#ENCODING_DOLBY_TRUEHD */ public static final int ENCODING_DOLBY_TRUEHD = AudioFormat.ENCODING_DOLBY_TRUEHD; - /** - * @see AudioFormat#CHANNEL_OUT_7POINT1_SURROUND - */ - @SuppressWarnings("deprecation") - public static final int CHANNEL_OUT_7POINT1_SURROUND = Util.SDK_INT < 23 - ? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; - /** * Stream types for an {@link android.media.AudioTrack}. */ @@ -271,24 +284,32 @@ public final class C { public static final int FLAG_AUDIBILITY_ENFORCED = android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED; - /** - * Usage types for {@link com.google.android.exoplayer2.audio.AudioAttributes}. - */ + /** Usage types for {@link com.google.android.exoplayer2.audio.AudioAttributes}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({USAGE_ALARM, USAGE_ASSISTANCE_ACCESSIBILITY, USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, - USAGE_ASSISTANCE_SONIFICATION, USAGE_GAME, USAGE_MEDIA, USAGE_NOTIFICATION, - USAGE_NOTIFICATION_COMMUNICATION_DELAYED, USAGE_NOTIFICATION_COMMUNICATION_INSTANT, - USAGE_NOTIFICATION_COMMUNICATION_REQUEST, USAGE_NOTIFICATION_EVENT, - USAGE_NOTIFICATION_RINGTONE, USAGE_UNKNOWN, USAGE_VOICE_COMMUNICATION, - USAGE_VOICE_COMMUNICATION_SIGNALLING}) + @IntDef({ + USAGE_ALARM, + USAGE_ASSISTANCE_ACCESSIBILITY, + USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, + USAGE_ASSISTANCE_SONIFICATION, + USAGE_ASSISTANT, + USAGE_GAME, + USAGE_MEDIA, + USAGE_NOTIFICATION, + USAGE_NOTIFICATION_COMMUNICATION_DELAYED, + USAGE_NOTIFICATION_COMMUNICATION_INSTANT, + USAGE_NOTIFICATION_COMMUNICATION_REQUEST, + USAGE_NOTIFICATION_EVENT, + USAGE_NOTIFICATION_RINGTONE, + USAGE_UNKNOWN, + USAGE_VOICE_COMMUNICATION, + USAGE_VOICE_COMMUNICATION_SIGNALLING + }) public @interface AudioUsage {} /** * @see android.media.AudioAttributes#USAGE_ALARM */ public static final int USAGE_ALARM = android.media.AudioAttributes.USAGE_ALARM; - /** - * @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY - */ + /** @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY */ public static final int USAGE_ASSISTANCE_ACCESSIBILITY = android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY; /** @@ -301,6 +322,8 @@ public final class C { */ public static final int USAGE_ASSISTANCE_SONIFICATION = android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION; + /** @see android.media.AudioAttributes#USAGE_ASSISTANT */ + public static final int USAGE_ASSISTANT = android.media.AudioAttributes.USAGE_ASSISTANT; /** * @see android.media.AudioAttributes#USAGE_GAME */ @@ -353,6 +376,29 @@ public final class C { public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING = android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING; + /** Audio focus types. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + AUDIOFOCUS_NONE, + AUDIOFOCUS_GAIN, + AUDIOFOCUS_GAIN_TRANSIENT, + AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, + AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE + }) + public @interface AudioFocusGain {} + /** @see AudioManager#AUDIOFOCUS_NONE */ + public static final int AUDIOFOCUS_NONE = AudioManager.AUDIOFOCUS_NONE; + /** @see AudioManager#AUDIOFOCUS_GAIN */ + public static final int AUDIOFOCUS_GAIN = AudioManager.AUDIOFOCUS_GAIN; + /** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT */ + public static final int AUDIOFOCUS_GAIN_TRANSIENT = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT; + /** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK */ + public static final int AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK = + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK; + /** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE */ + public static final int AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE = + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE; + /** * Flags which can apply to a buffer containing a media sample. */ @@ -368,14 +414,10 @@ public final class C { * Flag for empty buffers that signal that the end of the stream was reached. */ public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM; - /** - * Indicates that a buffer is (at least partially) encrypted. - */ - public static final int BUFFER_FLAG_ENCRYPTED = 0x40000000; - /** - * Indicates that a buffer should be decoded but not rendered. - */ - public static final int BUFFER_FLAG_DECODE_ONLY = 0x80000000; + /** Indicates that a buffer is (at least partially) encrypted. */ + public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000 + /** Indicates that a buffer should be decoded but not rendered. */ + public static final int BUFFER_FLAG_DECODE_ONLY = 1 << 31; // 0x80000000 /** * Video scaling modes for {@link MediaCodec}-based {@link Renderer}s. @@ -409,15 +451,13 @@ public final class C { * Indicates that the track should be selected if user preferences do not state otherwise. */ public static final int SELECTION_FLAG_DEFAULT = 1; - /** - * Indicates that the track must be displayed. Only applies to text tracks. - */ - public static final int SELECTION_FLAG_FORCED = 2; + /** Indicates that the track must be displayed. Only applies to text tracks. */ + public static final int SELECTION_FLAG_FORCED = 1 << 1; // 2 /** * Indicates that the player may choose to play the track in absence of an explicit user * preference. */ - public static final int SELECTION_FLAG_AUTOSELECT = 4; + public static final int SELECTION_FLAG_AUTOSELECT = 1 << 2; // 4 /** * Represents an undetermined language as an ISO 639 alpha-3 language code. @@ -469,32 +509,24 @@ public final class C { */ public static final int RESULT_FORMAT_READ = -5; - /** - * A data type constant for data of unknown or unspecified type. - */ + /** A data type constant for data of unknown or unspecified type. */ public static final int DATA_TYPE_UNKNOWN = 0; - /** - * A data type constant for media, typically containing media samples. - */ + /** A data type constant for media, typically containing media samples. */ public static final int DATA_TYPE_MEDIA = 1; - /** - * A data type constant for media, typically containing only initialization data. - */ + /** A data type constant for media, typically containing only initialization data. */ public static final int DATA_TYPE_MEDIA_INITIALIZATION = 2; - /** - * A data type constant for drm or encryption data. - */ + /** A data type constant for drm or encryption data. */ public static final int DATA_TYPE_DRM = 3; - /** - * A data type constant for a manifest file. - */ + /** A data type constant for a manifest file. */ public static final int DATA_TYPE_MANIFEST = 4; - /** - * A data type constant for time synchronization data. - */ + /** A data type constant for time synchronization data. */ public static final int DATA_TYPE_TIME_SYNCHRONIZATION = 5; /** A data type constant for ads loader data. */ public static final int DATA_TYPE_AD = 6; + /** + * A data type constant for live progressive media streams, typically containing media samples. + */ + public static final int DATA_TYPE_MEDIA_PROGRESSIVE_LIVE = 7; /** * Applications or extensions may define custom {@code DATA_TYPE_*} constants greater than or * equal to this value. @@ -694,6 +726,13 @@ public final class C { */ public static final int MSG_SET_SCALING_MODE = 4; + /** + * A type of a message that can be passed to an audio {@link Renderer} via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be an {@link AuxEffectInfo} + * instance representing an auxiliary audio effect for the underlying audio track. + */ + public static final int MSG_SET_AUX_EFFECT_INFO = 5; + /** * Applications or extensions may define custom {@code MSG_*} constants that can be passed to * {@link Renderer}s. These custom constants must be greater than or equal to this value. @@ -797,6 +836,45 @@ public final class C { */ public static final int PRIORITY_DOWNLOAD = PRIORITY_PLAYBACK - 1000; + /** Network connection type. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + NETWORK_TYPE_UNKNOWN, + NETWORK_TYPE_OFFLINE, + NETWORK_TYPE_WIFI, + NETWORK_TYPE_2G, + NETWORK_TYPE_3G, + NETWORK_TYPE_4G, + NETWORK_TYPE_CELLULAR_UNKNOWN, + NETWORK_TYPE_ETHERNET, + NETWORK_TYPE_OTHER + }) + public @interface NetworkType {} + /** Unknown network type. */ + public static final int NETWORK_TYPE_UNKNOWN = 0; + /** No network connection. */ + public static final int NETWORK_TYPE_OFFLINE = 1; + /** Network type for a Wifi connection. */ + public static final int NETWORK_TYPE_WIFI = 2; + /** Network type for a 2G cellular connection. */ + public static final int NETWORK_TYPE_2G = 3; + /** Network type for a 3G cellular connection. */ + public static final int NETWORK_TYPE_3G = 4; + /** Network type for a 4G cellular connection. */ + public static final int NETWORK_TYPE_4G = 5; + /** + * Network type for cellular connections which cannot be mapped to one of {@link + * #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, or {@link #NETWORK_TYPE_4G}. + */ + public static final int NETWORK_TYPE_CELLULAR_UNKNOWN = 6; + /** Network type for an Ethernet connection. */ + public static final int NETWORK_TYPE_ETHERNET = 7; + /** + * Network type for other connections which are not Wifi or cellular (e.g. Ethernet, VPN, + * Bluetooth). + */ + public static final int NETWORK_TYPE_OTHER = 8; + /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving * {@link #TIME_UNSET} and {@link #TIME_END_OF_SOURCE} values. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 39a6243933..5780f7b418 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -89,12 +89,13 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * model"> * *

    - *
  • It is strongly recommended that ExoPlayer instances are created and accessed from a single - * application thread. The application's main thread is ideal. Accessing an instance from - * multiple threads is discouraged as it may cause synchronization problems. - *
  • Registered listeners are called on the thread that created the ExoPlayer instance, unless - * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that - * case, registered listeners will be called on the application's main thread. + *
  • ExoPlayer instances must be accessed from the thread associated with {@link + * #getApplicationLooper()}. This Looper can be specified when creating the player, or this is + * the Looper of the thread the player is created on, or the Looper of the application's main + * thread if the player is created on a thread without Looper. + *
  • Registered listeners are called on the thread associated with {@link + * #getApplicationLooper()}. Note that this means registered listeners are called on the same + * thread which must be used to access the player. *
  • An internal playback thread is responsible for playback. Injected player components such as * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this * thread. @@ -178,13 +179,15 @@ public interface ExoPlayer extends Player { @Deprecated @RepeatMode int REPEAT_MODE_ALL = Player.REPEAT_MODE_ALL; - /** - * Gets the {@link Looper} associated with the playback thread. - * - * @return The {@link Looper} associated with the playback thread. - */ + /** Returns the {@link Looper} associated with the playback thread. */ Looper getPlaybackLooper(); + /** + * Returns the {@link Looper} associated with the application thread that's used to access the + * player and on which player events are received. + */ + Looper getApplicationLooper(); + /** * Prepares the player to play the provided {@link MediaSource}. Equivalent to * {@code prepare(mediaSource, true, true)}. @@ -239,4 +242,7 @@ public interface ExoPlayer extends Player { * @param seekParameters The seek parameters, or {@code null} to use the defaults. */ void setSeekParameters(@Nullable SeekParameters seekParameters); + + /** Returns the currently active {@link SeekParameters} of the player. */ + SeekParameters getSeekParameters(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index 8095ed9c64..b00a485843 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -16,18 +16,25 @@ package com.google.android.exoplayer2; import android.content.Context; +import android.os.Looper; import android.support.annotation.Nullable; import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.Util; /** * A factory for {@link ExoPlayer} instances. */ public final class ExoPlayerFactory { + private static @Nullable BandwidthMeter singletonBandwidthMeter; + private ExoPlayerFactory() {} /** @@ -36,13 +43,14 @@ public final class ExoPlayerFactory { * @param context A {@link Context}. * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. - * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}. + * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector, + * LoadControl)}. */ @Deprecated - public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, - LoadControl loadControl) { + public static SimpleExoPlayer newSimpleInstance( + Context context, TrackSelector trackSelector, LoadControl loadControl) { RenderersFactory renderersFactory = new DefaultRenderersFactory(context); - return newSimpleInstance(renderersFactory, trackSelector, loadControl); + return newSimpleInstance(context, renderersFactory, trackSelector, loadControl); } /** @@ -53,14 +61,18 @@ public final class ExoPlayerFactory { * @param loadControl The {@link LoadControl} that will be used by the instance. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. - * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}. + * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector, + * LoadControl)}. */ @Deprecated - public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, + public static SimpleExoPlayer newSimpleInstance( + Context context, + TrackSelector trackSelector, LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager) { RenderersFactory renderersFactory = new DefaultRenderersFactory(context); - return newSimpleInstance(renderersFactory, trackSelector, loadControl, drmSessionManager); + return newSimpleInstance( + context, renderersFactory, trackSelector, loadControl, drmSessionManager); } /** @@ -74,14 +86,19 @@ public final class ExoPlayerFactory { * @param extensionRendererMode The extension renderer mode, which determines if and how available * extension renderers are used. Note that extensions must be included in the application * build for them to be considered available. - * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}. + * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector, + * LoadControl)}. */ @Deprecated - public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, - LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, + public static SimpleExoPlayer newSimpleInstance( + Context context, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) { RenderersFactory renderersFactory = new DefaultRenderersFactory(context, extensionRendererMode); - return newSimpleInstance(renderersFactory, trackSelector, loadControl, drmSessionManager); + return newSimpleInstance( + context, renderersFactory, trackSelector, loadControl, drmSessionManager); } /** @@ -97,16 +114,21 @@ public final class ExoPlayerFactory { * build for them to be considered available. * @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to * seamlessly join an ongoing playback. - * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}. + * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector, + * LoadControl)}. */ @Deprecated - public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, - LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, + public static SimpleExoPlayer newSimpleInstance( + Context context, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { RenderersFactory renderersFactory = new DefaultRenderersFactory(context, extensionRendererMode, allowedVideoJoiningTimeMs); - return newSimpleInstance(renderersFactory, trackSelector, loadControl, drmSessionManager); + return newSimpleInstance( + context, renderersFactory, trackSelector, loadControl, drmSessionManager); } /** @@ -116,7 +138,7 @@ public final class ExoPlayerFactory { * @param trackSelector The {@link TrackSelector} that will be used by the instance. */ public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector) { - return newSimpleInstance(new DefaultRenderersFactory(context), trackSelector); + return newSimpleInstance(context, new DefaultRenderersFactory(context), trackSelector); } /** @@ -124,44 +146,74 @@ public final class ExoPlayerFactory { * * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector)}. The use + * of {@link SimpleExoPlayer#setAudioAttributes(AudioAttributes, boolean)} to manage audio + * focus will be unavailable for the {@link SimpleExoPlayer} returned by this method. */ - public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory, - TrackSelector trackSelector) { - return newSimpleInstance(renderersFactory, trackSelector, new DefaultLoadControl()); + @Deprecated + @SuppressWarnings("nullness:argument.type.incompatible") + public static SimpleExoPlayer newSimpleInstance( + RenderersFactory renderersFactory, TrackSelector trackSelector) { + return newSimpleInstance( + /* context= */ null, renderersFactory, trackSelector, new DefaultLoadControl()); } /** * Creates a {@link SimpleExoPlayer} instance. * + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + */ + public static SimpleExoPlayer newSimpleInstance( + Context context, RenderersFactory renderersFactory, TrackSelector trackSelector) { + return newSimpleInstance(context, renderersFactory, trackSelector, new DefaultLoadControl()); + } + + /** + * Creates a {@link SimpleExoPlayer} instance. + * + * @param context A {@link Context}. * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. */ public static SimpleExoPlayer newSimpleInstance( + Context context, RenderersFactory renderersFactory, TrackSelector trackSelector, @Nullable DrmSessionManager drmSessionManager) { return newSimpleInstance( - renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager); + context, renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager); } /** * Creates a {@link SimpleExoPlayer} instance. * + * @param context A {@link Context}. * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. */ - public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory, - TrackSelector trackSelector, LoadControl loadControl) { - return new SimpleExoPlayer( - renderersFactory, trackSelector, loadControl, /* drmSessionManager= */ null); + public static SimpleExoPlayer newSimpleInstance( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl) { + return newSimpleInstance( + context, + renderersFactory, + trackSelector, + loadControl, + /* drmSessionManager= */ null, + Util.getLooper()); } /** * Creates a {@link SimpleExoPlayer} instance. * + * @param context A {@link Context}. * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. @@ -169,16 +221,48 @@ public final class ExoPlayerFactory { * will not be used for DRM protected playbacks. */ public static SimpleExoPlayer newSimpleInstance( + Context context, RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager) { - return new SimpleExoPlayer(renderersFactory, trackSelector, loadControl, drmSessionManager); + return newSimpleInstance( + context, renderersFactory, trackSelector, loadControl, drmSessionManager, Util.getLooper()); } /** * Creates a {@link SimpleExoPlayer} instance. * + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance + * will not be used for DRM protected playbacks. + * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. + */ + public static SimpleExoPlayer newSimpleInstance( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, + BandwidthMeter bandwidthMeter) { + return newSimpleInstance( + context, + renderersFactory, + trackSelector, + loadControl, + drmSessionManager, + bandwidthMeter, + new AnalyticsCollector.Factory(), + Util.getLooper()); + } + + /** + * Creates a {@link SimpleExoPlayer} instance. + * + * @param context A {@link Context}. * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. @@ -188,13 +272,116 @@ public final class ExoPlayerFactory { * will collect and forward all player events. */ public static SimpleExoPlayer newSimpleInstance( + Context context, RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, AnalyticsCollector.Factory analyticsCollectorFactory) { + return newSimpleInstance( + context, + renderersFactory, + trackSelector, + loadControl, + drmSessionManager, + analyticsCollectorFactory, + Util.getLooper()); + } + + /** + * Creates a {@link SimpleExoPlayer} instance. + * + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance + * will not be used for DRM protected playbacks. + * @param looper The {@link Looper} which must be used for all calls to the player and which is + * used to call listeners on. + */ + public static SimpleExoPlayer newSimpleInstance( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, + Looper looper) { + return newSimpleInstance( + context, + renderersFactory, + trackSelector, + loadControl, + drmSessionManager, + new AnalyticsCollector.Factory(), + looper); + } + + /** + * Creates a {@link SimpleExoPlayer} instance. + * + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance + * will not be used for DRM protected playbacks. + * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that + * will collect and forward all player events. + * @param looper The {@link Looper} which must be used for all calls to the player and which is + * used to call listeners on. + */ + public static SimpleExoPlayer newSimpleInstance( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, + AnalyticsCollector.Factory analyticsCollectorFactory, + Looper looper) { + return newSimpleInstance( + context, + renderersFactory, + trackSelector, + loadControl, + drmSessionManager, + getDefaultBandwidthMeter(), + analyticsCollectorFactory, + looper); + } + + /** + * Creates a {@link SimpleExoPlayer} instance. + * + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance + * will not be used for DRM protected playbacks. + * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that + * will collect and forward all player events. + * @param looper The {@link Looper} which must be used for all calls to the player and which is + * used to call listeners on. + */ + public static SimpleExoPlayer newSimpleInstance( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, + BandwidthMeter bandwidthMeter, + AnalyticsCollector.Factory analyticsCollectorFactory, + Looper looper) { return new SimpleExoPlayer( - renderersFactory, trackSelector, loadControl, drmSessionManager, analyticsCollectorFactory); + context, + renderersFactory, + trackSelector, + loadControl, + drmSessionManager, + bandwidthMeter, + analyticsCollectorFactory, + looper); } /** @@ -216,7 +403,47 @@ public final class ExoPlayerFactory { */ public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { - return new ExoPlayerImpl(renderers, trackSelector, loadControl, Clock.DEFAULT); + return newInstance(renderers, trackSelector, loadControl, Util.getLooper()); } + /** + * Creates an {@link ExoPlayer} instance. + * + * @param renderers The {@link Renderer}s that will be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param looper The {@link Looper} which must be used for all calls to the player and which is + * used to call listeners on. + */ + public static ExoPlayer newInstance( + Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Looper looper) { + return newInstance(renderers, trackSelector, loadControl, getDefaultBandwidthMeter(), looper); + } + + /** + * Creates an {@link ExoPlayer} instance. + * + * @param renderers The {@link Renderer}s that will be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. + * @param looper The {@link Looper} which must be used for all calls to the player and which is + * used to call listeners on. + */ + public static ExoPlayer newInstance( + Renderer[] renderers, + TrackSelector trackSelector, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + Looper looper) { + return new ExoPlayerImpl( + renderers, trackSelector, loadControl, bandwidthMeter, Clock.DEFAULT, looper); + } + + private static synchronized BandwidthMeter getDefaultBandwidthMeter() { + if (singletonBandwidthMeter == null) { + singletonBandwidthMeter = new DefaultBandwidthMeter.Builder().build(); + } + return singletonBandwidthMeter; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 4125a203a6..648168816f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -30,11 +30,14 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; +import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; /** @@ -44,23 +47,33 @@ import java.util.concurrent.CopyOnWriteArraySet; private static final String TAG = "ExoPlayerImpl"; + /** + * This empty track selector result can only be used for {@link PlaybackInfo#trackSelectorResult} + * when the player does not have any track selection made (such as when player is reset, or when + * player seeks to an unprepared period). It will not be used as result of any {@link + * TrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray)} operation. + */ + /* package */ final TrackSelectorResult emptyTrackSelectorResult; + private final Renderer[] renderers; private final TrackSelector trackSelector; - private final TrackSelectorResult emptyTrackSelectorResult; private final Handler eventHandler; private final ExoPlayerImplInternal internalPlayer; private final Handler internalPlayerHandler; private final CopyOnWriteArraySet listeners; private final Timeline.Window window; private final Timeline.Period period; + private final ArrayDeque pendingPlaybackInfoUpdates; private boolean playWhenReady; + private boolean internalPlayWhenReady; private @RepeatMode int repeatMode; private boolean shuffleModeEnabled; private int pendingOperationAcks; private boolean hasPendingPrepare; private boolean hasPendingSeek; private PlaybackParameters playbackParameters; + private SeekParameters seekParameters; private @Nullable ExoPlaybackException playbackError; // Playback information when there is no pending seek/set source operation. @@ -77,11 +90,19 @@ import java.util.concurrent.CopyOnWriteArraySet; * @param renderers The {@link Renderer}s that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. * @param clock The {@link Clock} that will be used by the instance. + * @param looper The {@link Looper} which must be used for all calls to the player and which is + * used to call listeners on. */ @SuppressLint("HandlerLeak") public ExoPlayerImpl( - Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) { + Renderer[] renderers, + TrackSelector trackSelector, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + Clock clock, + Looper looper) { Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " [" + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]"); Assertions.checkState(renderers.length > 0); @@ -99,25 +120,23 @@ import java.util.concurrent.CopyOnWriteArraySet; window = new Timeline.Window(); period = new Timeline.Period(); playbackParameters = PlaybackParameters.DEFAULT; - Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); - eventHandler = new Handler(eventLooper) { - @Override - public void handleMessage(Message msg) { - ExoPlayerImpl.this.handleEvent(msg); - } - }; - playbackInfo = - new PlaybackInfo( - Timeline.EMPTY, - /* startPositionUs= */ 0, - TrackGroupArray.EMPTY, - emptyTrackSelectorResult); + seekParameters = SeekParameters.DEFAULT; + eventHandler = + new Handler(looper) { + @Override + public void handleMessage(Message msg) { + ExoPlayerImpl.this.handleEvent(msg); + } + }; + playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult); + pendingPlaybackInfoUpdates = new ArrayDeque<>(); internalPlayer = new ExoPlayerImplInternal( renderers, trackSelector, emptyTrackSelectorResult, loadControl, + bandwidthMeter, playWhenReady, repeatMode, shuffleModeEnabled, @@ -127,6 +146,11 @@ import java.util.concurrent.CopyOnWriteArraySet; internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); } + @Override + public AudioComponent getAudioComponent() { + return null; + } + @Override public VideoComponent getVideoComponent() { return null; @@ -142,6 +166,11 @@ import java.util.concurrent.CopyOnWriteArraySet; return internalPlayer.getPlaybackLooper(); } + @Override + public Looper getApplicationLooper() { + return eventHandler.getLooper(); + } + @Override public void addListener(Player.EventListener listener) { listeners.add(listener); @@ -185,18 +214,30 @@ import java.util.concurrent.CopyOnWriteArraySet; /* positionDiscontinuity= */ false, /* ignored */ DISCONTINUITY_REASON_INTERNAL, TIMELINE_CHANGE_REASON_RESET, - /* seekProcessed= */ false); + /* seekProcessed= */ false, + /* playWhenReadyChanged= */ false); } @Override public void setPlayWhenReady(boolean playWhenReady) { + setPlayWhenReady(playWhenReady, /* suppressPlayback= */ false); + } + + public void setPlayWhenReady(boolean playWhenReady, boolean suppressPlayback) { + boolean internalPlayWhenReady = playWhenReady && !suppressPlayback; + if (this.internalPlayWhenReady != internalPlayWhenReady) { + this.internalPlayWhenReady = internalPlayWhenReady; + internalPlayer.setPlayWhenReady(internalPlayWhenReady); + } if (this.playWhenReady != playWhenReady) { this.playWhenReady = playWhenReady; - internalPlayer.setPlayWhenReady(playWhenReady); - PlaybackInfo playbackInfo = this.playbackInfo; - for (Player.EventListener listener : listeners) { - listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState); - } + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + /* ignored */ TIMELINE_CHANGE_REASON_RESET, + /* seekProcessed= */ false, + /* playWhenReadyChanged= */ true); } } @@ -286,10 +327,10 @@ import java.util.concurrent.CopyOnWriteArraySet; } else { long windowPositionUs = positionMs == C.TIME_UNSET ? timeline.getWindow(windowIndex, window).getDefaultPositionUs() : C.msToUs(positionMs); - Pair periodIndexAndPositon = + Pair periodIndexAndPosition = timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); maskingWindowPositionMs = C.usToMs(windowPositionUs); - maskingPeriodIndex = periodIndexAndPositon.first; + maskingPeriodIndex = periodIndexAndPosition.first; } internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs)); for (Player.EventListener listener : listeners) { @@ -315,7 +356,15 @@ import java.util.concurrent.CopyOnWriteArraySet; if (seekParameters == null) { seekParameters = SeekParameters.DEFAULT; } - internalPlayer.setSeekParameters(seekParameters); + if (!this.seekParameters.equals(seekParameters)) { + this.seekParameters = seekParameters; + internalPlayer.setSeekParameters(seekParameters); + } + } + + @Override + public SeekParameters getSeekParameters() { + return seekParameters; } @Override @@ -352,7 +401,8 @@ import java.util.concurrent.CopyOnWriteArraySet; /* positionDiscontinuity= */ false, /* ignored */ DISCONTINUITY_REASON_INTERNAL, TIMELINE_CHANGE_REASON_RESET, - /* seekProcessed= */ false); + /* seekProcessed= */ false, + /* playWhenReadyChanged= */ false); } @Override @@ -461,29 +511,37 @@ import java.util.concurrent.CopyOnWriteArraySet; public long getCurrentPosition() { if (shouldMaskPosition()) { return maskingWindowPositionMs; + } else if (playbackInfo.periodId.isAd()) { + return C.usToMs(playbackInfo.positionUs); } else { - return playbackInfoPositionUsToWindowPositionMs(playbackInfo.positionUs); + return periodPositionUsToWindowPositionMs(playbackInfo.periodId, playbackInfo.positionUs); } } @Override public long getBufferedPosition() { - // TODO - Implement this properly. - if (shouldMaskPosition()) { - return maskingWindowPositionMs; - } else { - return playbackInfoPositionUsToWindowPositionMs(playbackInfo.bufferedPositionUs); + if (isPlayingAd()) { + return playbackInfo.loadingMediaPeriodId.equals(playbackInfo.periodId) + ? C.usToMs(playbackInfo.bufferedPositionUs) + : getDuration(); } + return getContentBufferedPosition(); } @Override public int getBufferedPercentage() { long position = getBufferedPosition(); long duration = getDuration(); - return position == C.TIME_UNSET || duration == C.TIME_UNSET ? 0 + return position == C.TIME_UNSET || duration == C.TIME_UNSET + ? 0 : (duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100)); } + @Override + public long getTotalBufferedDuration() { + return Math.max(0, C.usToMs(playbackInfo.totalBufferedDurationUs)); + } + @Override public boolean isCurrentWindowDynamic() { Timeline timeline = playbackInfo.timeline; @@ -521,6 +579,29 @@ import java.util.concurrent.CopyOnWriteArraySet; } } + @Override + public long getContentBufferedPosition() { + if (shouldMaskPosition()) { + return maskingWindowPositionMs; + } + if (playbackInfo.loadingMediaPeriodId.windowSequenceNumber + != playbackInfo.periodId.windowSequenceNumber) { + return playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); + } + long contentBufferedPositionUs = playbackInfo.bufferedPositionUs; + if (playbackInfo.loadingMediaPeriodId.isAd()) { + Timeline.Period loadingPeriod = + playbackInfo.timeline.getPeriod(playbackInfo.loadingMediaPeriodId.periodIndex, period); + contentBufferedPositionUs = + loadingPeriod.getAdGroupTimeUs(playbackInfo.loadingMediaPeriodId.adGroupIndex); + if (contentBufferedPositionUs == C.TIME_END_OF_SOURCE) { + contentBufferedPositionUs = loadingPeriod.durationUs; + } + } + return periodPositionUsToWindowPositionMs( + playbackInfo.loadingMediaPeriodId, contentBufferedPositionUs); + } + @Override public int getRendererCount() { return renderers.length; @@ -615,7 +696,8 @@ import java.util.concurrent.CopyOnWriteArraySet; positionDiscontinuity, positionDiscontinuityReason, timelineChangeReason, - seekProcessed); + seekProcessed, + /* playWhenReadyChanged= */ false); } } @@ -639,68 +721,133 @@ import java.util.concurrent.CopyOnWriteArraySet; playbackState, /* isLoading= */ false, resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, - resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult); + resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, + playbackInfo.periodId, + playbackInfo.startPositionUs, + /* totalBufferedDurationUs= */ 0, + playbackInfo.startPositionUs); } private void updatePlaybackInfo( - PlaybackInfo newPlaybackInfo, + PlaybackInfo playbackInfo, boolean positionDiscontinuity, @Player.DiscontinuityReason int positionDiscontinuityReason, @Player.TimelineChangeReason int timelineChangeReason, - boolean seekProcessed) { - boolean timelineOrManifestChanged = - playbackInfo.timeline != newPlaybackInfo.timeline - || playbackInfo.manifest != newPlaybackInfo.manifest; - boolean playbackStateChanged = playbackInfo.playbackState != newPlaybackInfo.playbackState; - boolean isLoadingChanged = playbackInfo.isLoading != newPlaybackInfo.isLoading; - boolean trackSelectorResultChanged = - playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult; - playbackInfo = newPlaybackInfo; - if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { - for (Player.EventListener listener : listeners) { - listener.onTimelineChanged( - playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason); - } + boolean seekProcessed, + boolean playWhenReadyChanged) { + boolean isRunningRecursiveListenerNotification = !pendingPlaybackInfoUpdates.isEmpty(); + pendingPlaybackInfoUpdates.addLast( + new PlaybackInfoUpdate( + playbackInfo, + /* previousPlaybackInfo= */ this.playbackInfo, + listeners, + trackSelector, + positionDiscontinuity, + positionDiscontinuityReason, + timelineChangeReason, + seekProcessed, + playWhenReady, + playWhenReadyChanged)); + // Assign playback info immediately such that all getters return the right values. + this.playbackInfo = playbackInfo; + if (isRunningRecursiveListenerNotification) { + return; } - if (positionDiscontinuity) { - for (Player.EventListener listener : listeners) { - listener.onPositionDiscontinuity(positionDiscontinuityReason); - } - } - if (trackSelectorResultChanged) { - trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info); - for (Player.EventListener listener : listeners) { - listener.onTracksChanged( - playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections); - } - } - if (isLoadingChanged) { - for (Player.EventListener listener : listeners) { - listener.onLoadingChanged(playbackInfo.isLoading); - } - } - if (playbackStateChanged) { - for (Player.EventListener listener : listeners) { - listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState); - } - } - if (seekProcessed) { - for (Player.EventListener listener : listeners) { - listener.onSeekProcessed(); - } + while (!pendingPlaybackInfoUpdates.isEmpty()) { + pendingPlaybackInfoUpdates.peekFirst().notifyListeners(); + pendingPlaybackInfoUpdates.removeFirst(); } } - private long playbackInfoPositionUsToWindowPositionMs(long positionUs) { + private long periodPositionUsToWindowPositionMs(MediaPeriodId periodId, long positionUs) { long positionMs = C.usToMs(positionUs); - if (!playbackInfo.periodId.isAd()) { - playbackInfo.timeline.getPeriod(playbackInfo.periodId.periodIndex, period); - positionMs += period.getPositionInWindowMs(); - } + playbackInfo.timeline.getPeriod(periodId.periodIndex, period); + positionMs += period.getPositionInWindowMs(); return positionMs; } private boolean shouldMaskPosition() { return playbackInfo.timeline.isEmpty() || pendingOperationAcks > 0; } + + private static final class PlaybackInfoUpdate { + + private final PlaybackInfo playbackInfo; + private final Set listeners; + private final TrackSelector trackSelector; + private final boolean positionDiscontinuity; + private final @Player.DiscontinuityReason int positionDiscontinuityReason; + private final @Player.TimelineChangeReason int timelineChangeReason; + private final boolean seekProcessed; + private final boolean playWhenReady; + private final boolean playbackStateOrPlayWhenReadyChanged; + private final boolean timelineOrManifestChanged; + private final boolean isLoadingChanged; + private final boolean trackSelectorResultChanged; + + public PlaybackInfoUpdate( + PlaybackInfo playbackInfo, + PlaybackInfo previousPlaybackInfo, + Set listeners, + TrackSelector trackSelector, + boolean positionDiscontinuity, + @Player.DiscontinuityReason int positionDiscontinuityReason, + @Player.TimelineChangeReason int timelineChangeReason, + boolean seekProcessed, + boolean playWhenReady, + boolean playWhenReadyChanged) { + this.playbackInfo = playbackInfo; + this.listeners = listeners; + this.trackSelector = trackSelector; + this.positionDiscontinuity = positionDiscontinuity; + this.positionDiscontinuityReason = positionDiscontinuityReason; + this.timelineChangeReason = timelineChangeReason; + this.seekProcessed = seekProcessed; + this.playWhenReady = playWhenReady; + playbackStateOrPlayWhenReadyChanged = + playWhenReadyChanged || previousPlaybackInfo.playbackState != playbackInfo.playbackState; + timelineOrManifestChanged = + previousPlaybackInfo.timeline != playbackInfo.timeline + || previousPlaybackInfo.manifest != playbackInfo.manifest; + isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading; + trackSelectorResultChanged = + previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult; + } + + public void notifyListeners() { + if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { + for (Player.EventListener listener : listeners) { + listener.onTimelineChanged( + playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason); + } + } + if (positionDiscontinuity) { + for (Player.EventListener listener : listeners) { + listener.onPositionDiscontinuity(positionDiscontinuityReason); + } + } + if (trackSelectorResultChanged) { + trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info); + for (Player.EventListener listener : listeners) { + listener.onTracksChanged( + playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections); + } + } + if (isLoadingChanged) { + for (Player.EventListener listener : listeners) { + listener.onLoadingChanged(playbackInfo.isLoading); + } + } + if (playbackStateOrPlayWhenReadyChanged) { + for (Player.EventListener listener : listeners) { + listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState); + } + } + if (seekProcessed) { + for (Player.EventListener listener : listeners) { + listener.onSeekProcessed(); + } + } + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index fc946804f4..7e54726daf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; +import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.HandlerWrapper; @@ -77,6 +78,7 @@ import java.util.Collections; private static final int MSG_SET_SHUFFLE_ENABLED = 13; private static final int MSG_SEND_MESSAGE = 14; private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 15; + private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 16; private static final int PREPARING_SOURCE_INTERVAL_MS = 10; private static final int RENDERING_INTERVAL_MS = 10; @@ -87,6 +89,7 @@ import java.util.Collections; private final TrackSelector trackSelector; private final TrackSelectorResult emptyTrackSelectorResult; private final LoadControl loadControl; + private final BandwidthMeter bandwidthMeter; private final HandlerWrapper handler; private final HandlerThread internalPlaybackThread; private final Handler eventHandler; @@ -123,6 +126,7 @@ import java.util.Collections; TrackSelector trackSelector, TrackSelectorResult emptyTrackSelectorResult, LoadControl loadControl, + BandwidthMeter bandwidthMeter, boolean playWhenReady, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled, @@ -133,6 +137,7 @@ import java.util.Collections; this.trackSelector = trackSelector; this.emptyTrackSelectorResult = emptyTrackSelectorResult; this.loadControl = loadControl; + this.bandwidthMeter = bandwidthMeter; this.playWhenReady = playWhenReady; this.repeatMode = repeatMode; this.shuffleModeEnabled = shuffleModeEnabled; @@ -146,11 +151,7 @@ import java.util.Collections; seekParameters = SeekParameters.DEFAULT; playbackInfo = - new PlaybackInfo( - Timeline.EMPTY, - /* startPositionUs= */ C.TIME_UNSET, - TrackGroupArray.EMPTY, - emptyTrackSelectorResult); + PlaybackInfo.createDummy(/* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult); playbackInfoUpdate = new PlaybackInfoUpdate(); rendererCapabilities = new RendererCapabilities[renderers.length]; for (int i = 0; i < renderers.length; i++) { @@ -162,7 +163,7 @@ import java.util.Collections; enabledRenderers = new Renderer[0]; window = new Timeline.Window(); period = new Timeline.Period(); - trackSelector.init(this); + trackSelector.init(/* listener= */ this, bandwidthMeter); // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can // not normally change to this priority" is incorrect. @@ -271,8 +272,9 @@ import java.util.Collections; @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget(); - updateTrackSelectionPlaybackSpeed(playbackParameters.speed); + handler + .obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL, playbackParameters) + .sendToTarget(); } // Handler.Callback implementation. @@ -324,6 +326,9 @@ import java.util.Collections; case MSG_TRACK_SELECTION_INVALIDATED: reselectTracksInternal(); break; + case MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL: + handlePlaybackParameters((PlaybackParameters) msg.obj); + break; case MSG_SEND_MESSAGE: sendMessageInternal((PlayerMessage) msg.obj); break; @@ -393,7 +398,11 @@ import java.util.Collections; loadControl.onPrepared(); this.mediaSource = mediaSource; setState(Player.STATE_BUFFERING); - mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener= */ this); + mediaSource.prepareSource( + player, + /* isTopLevelSource= */ true, + /* listener= */ this, + bandwidthMeter.getTransferListener()); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -419,6 +428,7 @@ import java.util.Collections; if (!queue.updateRepeatMode(repeatMode)) { seekToCurrentPosition(/* sendDiscontinuity= */ true); } + updateLoadingMediaPeriodId(); } private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled) @@ -427,6 +437,7 @@ import java.util.Collections; if (!queue.updateShuffleModeEnabled(shuffleModeEnabled)) { seekToCurrentPosition(/* sendDiscontinuity= */ true); } + updateLoadingMediaPeriodId(); } private void seekToCurrentPosition(boolean sendDiscontinuity) throws ExoPlaybackException { @@ -483,11 +494,12 @@ import java.util.Collections; playbackInfo.positionUs = periodPositionUs; } - // Update the buffered position. + // Update the buffered position and total buffered duration. + MediaPeriodHolder loadingPeriod = queue.getLoadingPeriod(); playbackInfo.bufferedPositionUs = - enabledRenderers.length == 0 - ? playingPeriodHolder.info.durationUs - : playingPeriodHolder.getBufferedPositionUs(/* convertEosToDuration= */ true); + loadingPeriod.getBufferedPositionUs(/* convertEosToDuration= */ true); + playbackInfo.totalBufferedDurationUs = + playbackInfo.bufferedPositionUs - loadingPeriod.toPeriodTime(rendererPositionUs); } private void doSomeWork() throws ExoPlaybackException, IOException { @@ -587,7 +599,7 @@ import java.util.Collections; if (resolvedSeekPosition == null) { // The seek position was valid for the timeline that it was performed into, but the // timeline has changed or is not ready and a suitable seek position could not be resolved. - periodId = new MediaPeriodId(getFirstPeriodIndex()); + periodId = getFirstMediaPeriodId(); periodPositionUs = C.TIME_UNSET; contentPositionUs = C.TIME_UNSET; seekPositionAdjusted = true; @@ -660,7 +672,7 @@ import java.util.Collections; MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); MediaPeriodHolder newPlayingPeriodHolder = oldPlayingPeriodHolder; while (newPlayingPeriodHolder != null) { - if (shouldKeepPeriodHolder(periodId, periodPositionUs, newPlayingPeriodHolder)) { + if (periodId.equals(newPlayingPeriodHolder.info.id) && newPlayingPeriodHolder.prepared) { queue.removeAfter(newPlayingPeriodHolder); break; } @@ -688,26 +700,17 @@ import java.util.Collections; maybeContinueLoading(); } else { queue.clear(/* keepFrontPeriodUid= */ true); + // New period has not been prepared. + playbackInfo = + playbackInfo.copyWithTrackInfo(TrackGroupArray.EMPTY, emptyTrackSelectorResult); resetRendererPosition(periodPositionUs); } + updateLoadingMediaPeriodId(); handler.sendEmptyMessage(MSG_DO_SOME_WORK); return periodPositionUs; } - private boolean shouldKeepPeriodHolder( - MediaPeriodId seekPeriodId, long positionUs, MediaPeriodHolder holder) { - if (seekPeriodId.equals(holder.info.id) && holder.prepared) { - playbackInfo.timeline.getPeriod(holder.info.id.periodIndex, period); - int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs); - if (nextAdGroupIndex == C.INDEX_UNSET - || period.getAdGroupTimeUs(nextAdGroupIndex) == holder.info.endPositionUs) { - return true; - } - } - return false; - } - private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException { rendererPositionUs = !queue.hasPlayingPeriod() @@ -749,12 +752,15 @@ import java.util.Collections; } } - private int getFirstPeriodIndex() { + private MediaPeriodId getFirstMediaPeriodId() { Timeline timeline = playbackInfo.timeline; - return timeline.isEmpty() - ? 0 - : timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) + if (timeline.isEmpty()) { + return PlaybackInfo.DUMMY_MEDIA_PERIOD_ID; + } + int firstPeriodIndex = + timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) .firstPeriodIndex; + return new MediaPeriodId(firstPeriodIndex); } private void resetInternal( @@ -785,18 +791,25 @@ import java.util.Collections; pendingMessages.clear(); nextPendingMessageIndex = 0; } + // Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored. + MediaPeriodId mediaPeriodId = resetPosition ? getFirstMediaPeriodId() : playbackInfo.periodId; + long startPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.positionUs; + long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs; playbackInfo = new PlaybackInfo( resetState ? Timeline.EMPTY : playbackInfo.timeline, resetState ? null : playbackInfo.manifest, - resetPosition ? new MediaPeriodId(getFirstPeriodIndex()) : playbackInfo.periodId, - // Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored. - resetPosition ? C.TIME_UNSET : playbackInfo.positionUs, - resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs, + mediaPeriodId, + startPositionUs, + contentPositionUs, playbackInfo.playbackState, /* isLoading= */ false, resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, - resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult); + resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, + mediaPeriodId, + startPositionUs, + /* totalBufferedDurationUs= */ 0, + startPositionUs); if (releaseMediaSource) { if (mediaSource != null) { mediaSource.releaseSource(/* listener= */ this); @@ -892,7 +905,7 @@ import java.util.Collections; pendingMessageInfo.setResolvedPosition( periodPosition.first, periodPosition.second, - playbackInfo.timeline.getPeriod(periodPosition.first, period, true).uid); + playbackInfo.timeline.getUidOfPeriod(periodPosition.first)); } else { // Position has been resolved for a previous timeline. Try to find the updated period index. int index = playbackInfo.timeline.getIndexOfPeriod(pendingMessageInfo.resolvedPeriodUid); @@ -1051,6 +1064,7 @@ import java.util.Collections; updateLoadControlTrackSelection(periodHolder.trackGroups, periodHolder.trackSelectorResult); } } + updateLoadingMediaPeriodId(); if (playbackInfo.playbackState != Player.STATE_ENDED) { maybeContinueLoading(); updatePlaybackPositions(); @@ -1142,8 +1156,15 @@ import java.util.Collections; playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); pendingPrepareCount = 0; if (pendingInitialSeekPosition != null) { - Pair periodPosition = - resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); + Pair periodPosition; + try { + periodPosition = + resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); + } catch (IllegalSeekPositionException e) { + playbackInfo = + playbackInfo.fromNewPosition(getFirstMediaPeriodId(), C.TIME_UNSET, C.TIME_UNSET); + throw e; + } pendingInitialSeekPosition = null; if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the @@ -1176,22 +1197,28 @@ import java.util.Collections; return; } - int playingPeriodIndex = playbackInfo.periodId.periodIndex; - long contentPositionUs = playbackInfo.contentPositionUs; if (oldTimeline.isEmpty()) { // If the old timeline is empty, the period queue is also empty. if (!timeline.isEmpty()) { - MediaPeriodId periodId = - queue.resolveMediaPeriodIdForAds(playingPeriodIndex, contentPositionUs); + Pair defaultPosition = + getPeriodPosition( + timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); + int periodIndex = defaultPosition.first; + long startPositionUs = defaultPosition.second; + MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, startPositionUs); playbackInfo = playbackInfo.fromNewPosition( - periodId, periodId.isAd() ? 0 : contentPositionUs, contentPositionUs); + periodId, + /* startPositionUs= */ periodId.isAd() ? 0 : startPositionUs, + /* contentPositionUs= */ startPositionUs); } return; } MediaPeriodHolder periodHolder = queue.getFrontPeriod(); - Object playingPeriodUid = periodHolder == null - ? oldTimeline.getPeriod(playingPeriodIndex, period, true).uid : periodHolder.uid; + int playingPeriodIndex = playbackInfo.periodId.periodIndex; + long contentPositionUs = playbackInfo.contentPositionUs; + Object playingPeriodUid = + periodHolder == null ? oldTimeline.getUidOfPeriod(playingPeriodIndex) : periodHolder.uid; int periodIndex = timeline.getIndexOfPeriod(playingPeriodUid); if (periodIndex == C.INDEX_UNSET) { // We didn't find the current period in the new timeline. Attempt to resolve a subsequent @@ -1208,11 +1235,10 @@ import java.util.Collections; newPeriodIndex = defaultPosition.first; contentPositionUs = defaultPosition.second; MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(newPeriodIndex, contentPositionUs); - timeline.getPeriod(newPeriodIndex, period, true); if (periodHolder != null) { // Clear the index of each holder that doesn't contain the default position. If a holder // contains the default position then update its index so it can be re-used when seeking. - Object newPeriodUid = period.uid; + Object newPeriodUid = timeline.getUidOfPeriod(newPeriodIndex); periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET); while (periodHolder.next != null) { periodHolder = periodHolder.next; @@ -1249,6 +1275,7 @@ import java.util.Collections; if (!queue.updateQueuedPeriods(playingPeriodId, rendererPositionUs)) { seekToCurrentPosition(/* sendDiscontinuity= */ false); } + updateLoadingMediaPeriodId(); } private void handleSourceInfoRefreshEndedPlayback() { @@ -1279,8 +1306,7 @@ import java.util.Collections; // We've reached the end of the old timeline. break; } - newPeriodIndex = newTimeline.getIndexOfPeriod( - oldTimeline.getPeriod(oldPeriodIndex, period, true).uid); + newPeriodIndex = newTimeline.getIndexOfPeriod(oldTimeline.getUidOfPeriod(oldPeriodIndex)); } return newPeriodIndex; } @@ -1324,8 +1350,7 @@ import java.util.Collections; return periodPosition; } // Attempt to find the mapped period in the internal timeline. - int periodIndex = timeline.getIndexOfPeriod( - seekTimeline.getPeriod(periodPosition.first, period, true).uid); + int periodIndex = timeline.getIndexOfPeriod(seekTimeline.getUidOfPeriod(periodPosition.first)); if (periodIndex != C.INDEX_UNSET) { // We successfully located the period in the internal timeline. return Pair.create(periodIndex, periodPosition.second); @@ -1381,8 +1406,9 @@ import java.util.Collections; MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); boolean advancedPlayingPeriod = false; - while (playWhenReady && playingPeriodHolder != readingPeriodHolder - && rendererPositionUs >= playingPeriodHolder.next.rendererPositionOffsetUs) { + while (playWhenReady + && playingPeriodHolder != readingPeriodHolder + && rendererPositionUs >= playingPeriodHolder.next.getStartPositionRendererTime()) { // All enabled renderers' streams have been read to the end, and the playback position reached // the end of the playing period, so advance playback to the next period. if (advancedPlayingPeriod) { @@ -1483,7 +1509,7 @@ import java.util.Collections; if (info == null) { mediaSource.maybeThrowSourceInfoRefreshError(); } else { - Object uid = playbackInfo.timeline.getPeriod(info.id.periodIndex, period, true).uid; + Object uid = playbackInfo.timeline.getUidOfPeriod(info.id.periodIndex); MediaPeriod mediaPeriod = queue.enqueueNextMediaPeriod( rendererCapabilities, @@ -1494,6 +1520,7 @@ import java.util.Collections; info); mediaPeriod.prepare(this, info.startPositionUs); setIsLoading(true); + updateLoadingMediaPeriodId(); } } } @@ -1525,6 +1552,17 @@ import java.util.Collections; maybeContinueLoading(); } + private void handlePlaybackParameters(PlaybackParameters playbackParameters) + throws ExoPlaybackException { + eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget(); + updateTrackSelectionPlaybackSpeed(playbackParameters.speed); + for (Renderer renderer : renderers) { + if (renderer != null) { + renderer.setOperatingRate(playbackParameters.speed); + } + } + } + private void maybeContinueLoading() { MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); long nextLoadPositionUs = loadingPeriodHolder.getNextLoadPositionUs(); @@ -1543,6 +1581,7 @@ import java.util.Collections; } } + @SuppressWarnings("ParameterNotNullable") private void updatePlayingPeriodRenderers(@Nullable MediaPeriodHolder oldPlayingPeriodHolder) throws ExoPlaybackException { MediaPeriodHolder newPlayingPeriodHolder = queue.getPlayingPeriod(); @@ -1619,6 +1658,13 @@ import java.util.Collections; && renderer.hasReadStreamToEnd(); } + private void updateLoadingMediaPeriodId() { + MediaPeriodHolder loadingMediaPeriodHolder = queue.getLoadingPeriod(); + MediaPeriodId loadingMediaPeriodId = + loadingMediaPeriodHolder == null ? playbackInfo.periodId : loadingMediaPeriodHolder.info.id; + playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(loadingMediaPeriodId); + } + @NonNull private static Format[] getFormats(TrackSelection newSelection) { // Build an array of formats contained by the selection. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 98d5fe91b7..b8bf0e8813 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.8.0"; + public static final String VERSION = "2.8.4"; /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.4"; /** * The version of the library expressed as an integer, for example 1002003. @@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2008000; + public static final int VERSION_INT = 2008004; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} @@ -51,6 +51,9 @@ public final class ExoPlayerLibraryInfo { */ public static final boolean ASSERTIONS_ENABLED = true; + /** Whether an exception should be thrown in case of an OpenGl error. */ + public static final boolean GL_ASSERTIONS_ENABLED = false; + /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.TraceUtil} * trace enabled. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index 61d416da09..4c6e1838a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -46,6 +46,9 @@ public final class Format implements Parcelable { /** An identifier for the format, or null if unknown or not applicable. */ public final @Nullable String id; + /** The human readable label, or null if unknown or not applicable. */ + public final @Nullable String label; + /** * The average bandwidth in bits per second, or {@link #NO_VALUE} if unknown or not applicable. */ @@ -80,6 +83,13 @@ public final class Format implements Parcelable { /** DRM initialization data if the stream is protected, or null otherwise. */ public final @Nullable DrmInitData drmInitData; + /** + * For samples that contain subsamples, this is an offset that should be added to subsample + * timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are + * relative to the timestamps of their parent samples. + */ + public final long subsampleOffsetUs; + // Video specific. /** @@ -125,12 +135,12 @@ public final class Format implements Parcelable { public final int sampleRate; /** * The encoding for PCM audio streams. If {@link #sampleMimeType} is {@link MimeTypes#AUDIO_RAW} - * then one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT}, - * {@link C#ENCODING_PCM_24BIT} and {@link C#ENCODING_PCM_32BIT}. Set to {@link #NO_VALUE} for - * other media types. + * then one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT}, {@link + * C#ENCODING_PCM_24BIT}, {@link C#ENCODING_PCM_32BIT}, {@link C#ENCODING_PCM_FLOAT}, {@link + * C#ENCODING_PCM_MU_LAW} or {@link C#ENCODING_PCM_A_LAW}. Set to {@link #NO_VALUE} for other + * media types. */ - @C.PcmEncoding - public final int pcmEncoding; + public final @C.PcmEncoding int pcmEncoding; /** * The number of frames to trim from the start of the decoded audio stream, or 0 if not * applicable. @@ -141,15 +151,6 @@ public final class Format implements Parcelable { */ public final int encoderPadding; - // Text specific. - - /** - * For samples that contain subsamples, this is an offset that should be added to subsample - * timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are - * relative to the timestamps of their parent samples. - */ - public final long subsampleOffsetUs; - // Audio and text specific. /** @@ -171,6 +172,7 @@ public final class Format implements Parcelable { // Video. + @Deprecated public static Format createVideoContainerFormat( @Nullable String id, @Nullable String containerMimeType, @@ -180,12 +182,62 @@ public final class Format implements Parcelable { int width, int height, float frameRate, - List initializationData, + @Nullable List initializationData, @C.SelectionFlags int selectionFlags) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width, - height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, selectionFlags, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, - initializationData, null, null); + return createVideoContainerFormat( + id, + /* label= */ null, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + width, + height, + frameRate, + initializationData, + selectionFlags); + } + + public static Format createVideoContainerFormat( + @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, + String sampleMimeType, + String codecs, + int bitrate, + int width, + int height, + float frameRate, + @Nullable List initializationData, + @C.SelectionFlags int selectionFlags) { + return new Format( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + /* maxInputSize= */ NO_VALUE, + width, + height, + frameRate, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + /* channelCount= */ NO_VALUE, + /* sampleRate= */ NO_VALUE, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + selectionFlags, + /* language= */ null, + /* accessibilityChannel= */ NO_VALUE, + OFFSET_SAMPLE_RELATIVE, + initializationData, + /* drmInitData= */ null, + /* metadata= */ null); } public static Format createVideoSampleFormat( @@ -197,10 +249,21 @@ public final class Format implements Parcelable { int width, int height, float frameRate, - List initializationData, + @Nullable List initializationData, @Nullable DrmInitData drmInitData) { - return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width, - height, frameRate, initializationData, NO_VALUE, NO_VALUE, drmInitData); + return createVideoSampleFormat( + id, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + width, + height, + frameRate, + initializationData, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + drmInitData); } public static Format createVideoSampleFormat( @@ -212,13 +275,26 @@ public final class Format implements Parcelable { int width, int height, float frameRate, - List initializationData, + @Nullable List initializationData, int rotationDegrees, float pixelWidthHeightRatio, @Nullable DrmInitData drmInitData) { - return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width, - height, frameRate, initializationData, rotationDegrees, pixelWidthHeightRatio, null, - NO_VALUE, null, drmInitData); + return createVideoSampleFormat( + id, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + width, + height, + frameRate, + initializationData, + rotationDegrees, + pixelWidthHeightRatio, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + drmInitData); } public static Format createVideoSampleFormat( @@ -230,21 +306,46 @@ public final class Format implements Parcelable { int width, int height, float frameRate, - List initializationData, + @Nullable List initializationData, int rotationDegrees, float pixelWidthHeightRatio, byte[] projectionData, @C.StereoMode int stereoMode, @Nullable ColorInfo colorInfo, @Nullable DrmInitData drmInitData) { - return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height, - frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, - colorInfo, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE, - OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, null); + return new Format( + id, + /* label= */ null, + /* containerMimeType= */ null, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + /* channelCount= */ NO_VALUE, + /* sampleRate= */ NO_VALUE, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + /* selectionFlags= */ 0, + /* language= */ null, + /* accessibilityChannel= */ NO_VALUE, + OFFSET_SAMPLE_RELATIVE, + initializationData, + drmInitData, + /* metadata= */ null); } // Audio. + @Deprecated public static Format createAudioContainerFormat( @Nullable String id, @Nullable String containerMimeType, @@ -253,13 +354,63 @@ public final class Format implements Parcelable { int bitrate, int channelCount, int sampleRate, - List initializationData, + @Nullable List initializationData, @C.SelectionFlags int selectionFlags, @Nullable String language) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate, - NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, - initializationData, null, null); + return createAudioContainerFormat( + id, + /* label= */ null, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + channelCount, + sampleRate, + initializationData, + selectionFlags, + language); + } + + public static Format createAudioContainerFormat( + @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int channelCount, + int sampleRate, + @Nullable List initializationData, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { + return new Format( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + /* maxInputSize= */ NO_VALUE, + /* width= */ NO_VALUE, + /* height= */ NO_VALUE, + /* frameRate= */ NO_VALUE, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + channelCount, + sampleRate, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + selectionFlags, + language, + /* accessibilityChannel= */ NO_VALUE, + OFFSET_SAMPLE_RELATIVE, + initializationData, + /* drmInitData= */ null, + /* metadata= */ null); } public static Format createAudioSampleFormat( @@ -270,12 +421,23 @@ public final class Format implements Parcelable { int maxInputSize, int channelCount, int sampleRate, - List initializationData, + @Nullable List initializationData, @Nullable DrmInitData drmInitData, @C.SelectionFlags int selectionFlags, @Nullable String language) { - return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount, - sampleRate, NO_VALUE, initializationData, drmInitData, selectionFlags, language); + return createAudioSampleFormat( + id, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + channelCount, + sampleRate, + /* pcmEncoding= */ NO_VALUE, + initializationData, + drmInitData, + selectionFlags, + language); } public static Format createAudioSampleFormat( @@ -287,13 +449,26 @@ public final class Format implements Parcelable { int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, - List initializationData, + @Nullable List initializationData, @Nullable DrmInitData drmInitData, @C.SelectionFlags int selectionFlags, @Nullable String language) { - return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount, - sampleRate, pcmEncoding, NO_VALUE, NO_VALUE, initializationData, drmInitData, - selectionFlags, language, null); + return createAudioSampleFormat( + id, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + channelCount, + sampleRate, + pcmEncoding, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + initializationData, + drmInitData, + selectionFlags, + language, + /* metadata= */ null); } public static Format createAudioSampleFormat( @@ -307,19 +482,44 @@ public final class Format implements Parcelable { @C.PcmEncoding int pcmEncoding, int encoderDelay, int encoderPadding, - List initializationData, + @Nullable List initializationData, @Nullable DrmInitData drmInitData, @C.SelectionFlags int selectionFlags, @Nullable String language, @Nullable Metadata metadata) { - return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate, pcmEncoding, - encoderDelay, encoderPadding, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, - initializationData, drmInitData, metadata); + return new Format( + id, + /* label= */ null, + /* containerMimeType= */ null, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + /* width= */ NO_VALUE, + /* height= */ NO_VALUE, + /* frameRate= */ NO_VALUE, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + selectionFlags, + language, + /* accessibilityChannel= */ NO_VALUE, + OFFSET_SAMPLE_RELATIVE, + initializationData, + drmInitData, + metadata); } // Text. + @Deprecated public static Format createTextContainerFormat( @Nullable String id, @Nullable String containerMimeType, @@ -328,12 +528,41 @@ public final class Format implements Parcelable { int bitrate, @C.SelectionFlags int selectionFlags, @Nullable String language) { - return createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate, - selectionFlags, language, NO_VALUE); + return createTextContainerFormat( + id, + /* label= */ null, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + selectionFlags, + language); } public static Format createTextContainerFormat( @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { + return createTextContainerFormat( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + selectionFlags, + language, + /* accessibilityChannel= */ NO_VALUE); + } + + public static Format createTextContainerFormat( + @Nullable String id, + @Nullable String label, @Nullable String containerMimeType, @Nullable String sampleMimeType, @Nullable String codecs, @@ -341,10 +570,34 @@ public final class Format implements Parcelable { @C.SelectionFlags int selectionFlags, @Nullable String language, int accessibilityChannel) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, accessibilityChannel, - OFFSET_SAMPLE_RELATIVE, null, null, null); + return new Format( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + /* maxInputSize= */ NO_VALUE, + /* width= */ NO_VALUE, + /* height= */ NO_VALUE, + /* frameRate= */ NO_VALUE, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + /* channelCount= */ NO_VALUE, + /* sampleRate= */ NO_VALUE, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + selectionFlags, + language, + accessibilityChannel, + OFFSET_SAMPLE_RELATIVE, + /* initializationData= */ null, + /* drmInitData= */ null, + /* metadata= */ null); } public static Format createTextSampleFormat( @@ -361,8 +614,17 @@ public final class Format implements Parcelable { @C.SelectionFlags int selectionFlags, @Nullable String language, @Nullable DrmInitData drmInitData) { - return createTextSampleFormat(id, sampleMimeType, null, NO_VALUE, selectionFlags, language, - NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.emptyList()); + return createTextSampleFormat( + id, + sampleMimeType, + /* codecs= */ null, + /* bitrate= */ NO_VALUE, + selectionFlags, + language, + NO_VALUE, + drmInitData, + OFFSET_SAMPLE_RELATIVE, + Collections.emptyList()); } public static Format createTextSampleFormat( @@ -374,8 +636,17 @@ public final class Format implements Parcelable { @Nullable String language, int accessibilityChannel, @Nullable DrmInitData drmInitData) { - return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, - accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.emptyList()); + return createTextSampleFormat( + id, + sampleMimeType, + codecs, + bitrate, + selectionFlags, + language, + accessibilityChannel, + drmInitData, + OFFSET_SAMPLE_RELATIVE, + Collections.emptyList()); } public static Format createTextSampleFormat( @@ -387,8 +658,17 @@ public final class Format implements Parcelable { @Nullable String language, @Nullable DrmInitData drmInitData, long subsampleOffsetUs) { - return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, - NO_VALUE, drmInitData, subsampleOffsetUs, Collections.emptyList()); + return createTextSampleFormat( + id, + sampleMimeType, + codecs, + bitrate, + selectionFlags, + language, + /* accessibilityChannel= */ NO_VALUE, + drmInitData, + subsampleOffsetUs, + Collections.emptyList()); } public static Format createTextSampleFormat( @@ -402,10 +682,34 @@ public final class Format implements Parcelable { @Nullable DrmInitData drmInitData, long subsampleOffsetUs, List initializationData) { - return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, - initializationData, drmInitData, null); + return new Format( + id, + /* label= */ null, + /* containerMimeType= */ null, + sampleMimeType, + codecs, + bitrate, + /* maxInputSize= */ NO_VALUE, + /* width= */ NO_VALUE, + /* height= */ NO_VALUE, + /* frameRate= */ NO_VALUE, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + /* channelCount= */ NO_VALUE, + /* sampleRate= */ NO_VALUE, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + selectionFlags, + language, + accessibilityChannel, + subsampleOffsetUs, + initializationData, + drmInitData, + /* metadata= */ null); } // Image. @@ -416,40 +720,42 @@ public final class Format implements Parcelable { @Nullable String codecs, int bitrate, @C.SelectionFlags int selectionFlags, - List initializationData, + @Nullable List initializationData, @Nullable String language, @Nullable DrmInitData drmInitData) { return new Format( id, - null, + /* label= */ null, + /* containerMimeType= */ null, sampleMimeType, codecs, bitrate, - NO_VALUE, - NO_VALUE, - NO_VALUE, - NO_VALUE, - NO_VALUE, - NO_VALUE, - null, - NO_VALUE, - null, - NO_VALUE, - NO_VALUE, - NO_VALUE, - NO_VALUE, - NO_VALUE, + /* maxInputSize= */ NO_VALUE, + /* width= */ NO_VALUE, + /* height= */ NO_VALUE, + /* frameRate= */ NO_VALUE, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + /* channelCount= */ NO_VALUE, + /* sampleRate= */ NO_VALUE, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, selectionFlags, language, - NO_VALUE, + /* accessibilityChannel= */ NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, - null); + /* metadata=*/ null); } // Generic. + @Deprecated public static Format createContainerFormat( @Nullable String id, @Nullable String containerMimeType, @@ -458,17 +764,86 @@ public final class Format implements Parcelable { int bitrate, @C.SelectionFlags int selectionFlags, @Nullable String language) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null, - null); + return createContainerFormat( + id, + /* label= */ null, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + selectionFlags, + language); + } + + public static Format createContainerFormat( + @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { + return new Format( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + /* maxInputSize= */ NO_VALUE, + /* width= */ NO_VALUE, + /* height= */ NO_VALUE, + /* frameRate= */ NO_VALUE, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + /* channelCount= */ NO_VALUE, + /* sampleRate= */ NO_VALUE, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + selectionFlags, + language, + /* accessibilityChannel= */ NO_VALUE, + OFFSET_SAMPLE_RELATIVE, + /* initializationData= */ null, + /* drmInitData= */ null, + /* metadata= */ null); } public static Format createSampleFormat( @Nullable String id, @Nullable String sampleMimeType, long subsampleOffsetUs) { - return new Format(id, null, sampleMimeType, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, 0, null, NO_VALUE, subsampleOffsetUs, null, null, null); + return new Format( + id, + /* label= */ null, + /* containerMimeType= */ null, + sampleMimeType, + /* codecs= */ null, + /* bitrate= */ NO_VALUE, + /* maxInputSize= */ NO_VALUE, + /* width= */ NO_VALUE, + /* height= */ NO_VALUE, + /* frameRate= */ NO_VALUE, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + /* channelCount= */ NO_VALUE, + /* sampleRate= */ NO_VALUE, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + /* selectionFlags= */ 0, + /* language= */ null, + /* accessibilityChannel= */ NO_VALUE, + subsampleOffsetUs, + /* initializationData= */ null, + /* drmInitData= */ null, + /* metadata= */ null); } public static Format createSampleFormat( @@ -477,13 +852,39 @@ public final class Format implements Parcelable { @Nullable String codecs, int bitrate, @Nullable DrmInitData drmInitData) { - return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, drmInitData, null); + return new Format( + id, + /* label= */ null, + /* containerMimeType= */ null, + sampleMimeType, + codecs, + bitrate, + /* maxInputSize= */ NO_VALUE, + /* width= */ NO_VALUE, + /* height= */ NO_VALUE, + /* frameRate= */ NO_VALUE, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + /* channelCount= */ NO_VALUE, + /* sampleRate= */ NO_VALUE, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + /* selectionFlags= */ 0, + /* language= */ null, + /* accessibilityChannel= */ NO_VALUE, + OFFSET_SAMPLE_RELATIVE, + /* initializationData= */ null, + drmInitData, + /* metadata= */ null); } /* package */ Format( @Nullable String id, + @Nullable String label, @Nullable String containerMimeType, @Nullable String sampleMimeType, @Nullable String codecs, @@ -510,6 +911,7 @@ public final class Format implements Parcelable { @Nullable DrmInitData drmInitData, @Nullable Metadata metadata) { this.id = id; + this.label = label; this.containerMimeType = containerMimeType; this.sampleMimeType = sampleMimeType; this.codecs = codecs; @@ -533,8 +935,8 @@ public final class Format implements Parcelable { this.language = language; this.accessibilityChannel = accessibilityChannel; this.subsampleOffsetUs = subsampleOffsetUs; - this.initializationData = initializationData == null ? Collections.emptyList() - : initializationData; + this.initializationData = + initializationData == null ? Collections.emptyList() : initializationData; this.drmInitData = drmInitData; this.metadata = metadata; } @@ -542,6 +944,7 @@ public final class Format implements Parcelable { @SuppressWarnings("ResourceType") /* package */ Format(Parcel in) { id = in.readString(); + label = in.readString(); containerMimeType = in.readString(); sampleMimeType = in.readString(); codecs = in.readString(); @@ -575,23 +978,70 @@ public final class Format implements Parcelable { } public Format copyWithMaxInputSize(int maxInputSize) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, - height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, - colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + return new Format( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + selectionFlags, + language, + accessibilityChannel, + subsampleOffsetUs, + initializationData, + drmInitData, + metadata); } public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, - height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, - colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + return new Format( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + selectionFlags, + language, + accessibilityChannel, + subsampleOffsetUs, + initializationData, + drmInitData, + metadata); } public Format copyWithContainerInfo( @Nullable String id, + @Nullable String label, @Nullable String sampleMimeType, @Nullable String codecs, int bitrate, @@ -599,11 +1049,34 @@ public final class Format implements Parcelable { int height, @C.SelectionFlags int selectionFlags, @Nullable String language) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, - height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, - colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + return new Format( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + selectionFlags, + language, + accessibilityChannel, + subsampleOffsetUs, + initializationData, + drmInitData, + metadata); } @SuppressWarnings("ReferenceEquality") @@ -612,51 +1085,193 @@ public final class Format implements Parcelable { // No need to copy from ourselves. return this; } + + int trackType = MimeTypes.getTrackType(sampleMimeType); + + // Use manifest value only. String id = manifestFormat.id; - String codecs = this.codecs == null ? manifestFormat.codecs : this.codecs; + + // Prefer manifest values, but fill in from sample format if missing. + String label = manifestFormat.label != null ? manifestFormat.label : this.label; + String language = this.language; + if ((trackType == C.TRACK_TYPE_TEXT || trackType == C.TRACK_TYPE_AUDIO) + && manifestFormat.language != null) { + language = manifestFormat.language; + } + + // Prefer sample format values, but fill in from manifest if missing. int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate; - float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate; - @C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags; - String language = this.language == null ? manifestFormat.language : this.language; + String codecs = this.codecs; + if (codecs == null) { + // The manifest format may be muxed, so filter only codecs of this format's type. If we still + // have more than one codec then we're unable to uniquely identify which codec to fill in. + String codecsOfType = Util.getCodecsOfType(manifestFormat.codecs, trackType); + if (Util.splitCodecs(codecsOfType).length == 1) { + codecs = codecsOfType; + } + } + float frameRate = this.frameRate; + if (frameRate == NO_VALUE && trackType == C.TRACK_TYPE_VIDEO) { + frameRate = manifestFormat.frameRate; + } + + // Merge manifest and sample format values. + @C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags; DrmInitData drmInitData = DrmInitData.createSessionCreationData(manifestFormat.drmInitData, this.drmInitData); - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, - height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, - colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + + return new Format( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + selectionFlags, + language, + accessibilityChannel, + subsampleOffsetUs, + initializationData, + drmInitData, + metadata); } public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, - height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, - colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + return new Format( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + selectionFlags, + language, + accessibilityChannel, + subsampleOffsetUs, + initializationData, + drmInitData, + metadata); } public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, - height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, - colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + return new Format( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + selectionFlags, + language, + accessibilityChannel, + subsampleOffsetUs, + initializationData, + drmInitData, + metadata); } public Format copyWithMetadata(@Nullable Metadata metadata) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, - height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, - colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + return new Format( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + selectionFlags, + language, + accessibilityChannel, + subsampleOffsetUs, + initializationData, + drmInitData, + metadata); } public Format copyWithRotationDegrees(int rotationDegrees) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, - height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, - colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + return new Format( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + selectionFlags, + language, + accessibilityChannel, + subsampleOffsetUs, + initializationData, + drmInitData, + metadata); } /** @@ -669,9 +1284,32 @@ public final class Format implements Parcelable { @Override public String toString() { - return "Format(" + id + ", " + containerMimeType + ", " + sampleMimeType + ", " + bitrate + ", " - + language + ", [" + width + ", " + height + ", " + frameRate + "]" - + ", [" + channelCount + ", " + sampleRate + "])"; + return "Format(" + + id + + ", " + + label + + ", " + + containerMimeType + + ", " + + sampleMimeType + + ", " + + codecs + + ", " + + bitrate + + ", " + + language + + ", [" + + width + + ", " + + height + + ", " + + frameRate + + "]" + + ", [" + + channelCount + + ", " + + sampleRate + + "])"; } @Override @@ -691,6 +1329,18 @@ public final class Format implements Parcelable { result = 31 * result + accessibilityChannel; result = 31 * result + (drmInitData == null ? 0 : drmInitData.hashCode()); result = 31 * result + (metadata == null ? 0 : metadata.hashCode()); + result = 31 * result + (label != null ? label.hashCode() : 0); + result = 31 * result + maxInputSize; + result = 31 * result + (int) subsampleOffsetUs; + result = 31 * result + Float.floatToIntBits(frameRate); + result = 31 * result + Float.floatToIntBits(pixelWidthHeightRatio); + result = 31 * result + rotationDegrees; + result = 31 * result + stereoMode; + result = 31 * result + pcmEncoding; + result = 31 * result + encoderDelay; + result = 31 * result + encoderPadding; + result = 31 * result + selectionFlags; + // Not all of the fields are included to keep the calculation quick enough. hashCode = result; } return hashCode; @@ -705,13 +1355,16 @@ public final class Format implements Parcelable { return false; } Format other = (Format) obj; + if (hashCode != 0 && other.hashCode != 0 && hashCode != other.hashCode) { + return false; + } return bitrate == other.bitrate && maxInputSize == other.maxInputSize && width == other.width && height == other.height - && frameRate == other.frameRate + && Float.compare(frameRate, other.frameRate) == 0 && rotationDegrees == other.rotationDegrees - && pixelWidthHeightRatio == other.pixelWidthHeightRatio + && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0 && stereoMode == other.stereoMode && channelCount == other.channelCount && sampleRate == other.sampleRate @@ -721,6 +1374,7 @@ public final class Format implements Parcelable { && subsampleOffsetUs == other.subsampleOffsetUs && selectionFlags == other.selectionFlags && Util.areEqual(id, other.id) + && Util.areEqual(label, other.label) && Util.areEqual(language, other.language) && accessibilityChannel == other.accessibilityChannel && Util.areEqual(containerMimeType, other.containerMimeType) @@ -767,6 +1421,9 @@ public final class Format implements Parcelable { if (format.bitrate != Format.NO_VALUE) { builder.append(", bitrate=").append(format.bitrate); } + if (format.codecs != null) { + builder.append(", codecs=").append(format.codecs); + } if (format.width != Format.NO_VALUE && format.height != Format.NO_VALUE) { builder.append(", res=").append(format.width).append("x").append(format.height); } @@ -782,6 +1439,9 @@ public final class Format implements Parcelable { if (format.language != null) { builder.append(", language=").append(format.language); } + if (format.label != null) { + builder.append(", label=").append(format.label); + } return builder.toString(); } @@ -795,6 +1455,7 @@ public final class Format implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(id); + dest.writeString(label); dest.writeString(containerMimeType); dest.writeString(sampleMimeType); dest.writeString(codecs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java index b26787517e..8c7ba1eb91 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java @@ -15,14 +15,13 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.Nullable; + /** * Holds a {@link Format}. */ public final class FormatHolder { - /** - * The held {@link Format}. - */ - public Format format; - + /** The held {@link Format}. */ + public @Nullable Format format; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java index 2f71d0d547..a74a2ac1ca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java @@ -39,7 +39,6 @@ import com.google.android.exoplayer2.util.Assertions; public final SampleStream[] sampleStreams; public final boolean[] mayRetainStreamFlags; - public long rendererPositionOffsetUs; public boolean prepared; public boolean hasEnabledTracks; public MediaPeriodInfo info; @@ -51,6 +50,7 @@ import com.google.android.exoplayer2.util.Assertions; private final TrackSelector trackSelector; private final MediaSource mediaSource; + private long rendererPositionOffsetUs; private TrackSelectorResult periodTrackSelectorResult; /** @@ -82,13 +82,13 @@ import com.google.android.exoplayer2.util.Assertions; sampleStreams = new SampleStream[rendererCapabilities.length]; mayRetainStreamFlags = new boolean[rendererCapabilities.length]; MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, allocator); - if (info.endPositionUs != C.TIME_END_OF_SOURCE) { + if (info.id.endPositionUs != C.TIME_END_OF_SOURCE) { mediaPeriod = new ClippingMediaPeriod( mediaPeriod, /* enableInitialDiscontinuity= */ true, /* startUs= */ 0, - info.endPositionUs); + info.id.endPositionUs); } this.mediaPeriod = mediaPeriod; } @@ -105,6 +105,10 @@ import com.google.android.exoplayer2.util.Assertions; return rendererPositionOffsetUs; } + public long getStartPositionRendererTime() { + return info.startPositionUs + rendererPositionOffsetUs; + } + public boolean isFullyBuffered() { return prepared && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE); @@ -127,7 +131,8 @@ import com.google.android.exoplayer2.util.Assertions; if (!prepared) { return info.startPositionUs; } - long bufferedPositionUs = mediaPeriod.getBufferedPositionUs(); + long bufferedPositionUs = + hasEnabledTracks ? mediaPeriod.getBufferedPositionUs() : C.TIME_END_OF_SOURCE; return bufferedPositionUs == C.TIME_END_OF_SOURCE && convertEosToDuration ? info.durationUs : bufferedPositionUs; @@ -218,7 +223,7 @@ import com.google.android.exoplayer2.util.Assertions; public void release() { updatePeriodTrackSelectorResult(null); try { - if (info.endPositionUs != C.TIME_END_OF_SOURCE) { + if (info.id.endPositionUs != C.TIME_END_OF_SOURCE) { mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); } else { mediaSource.releasePeriod(mediaPeriod); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java index fce1780b71..b887e8222e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java @@ -25,18 +25,13 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; public final MediaPeriodId id; /** The start position of the media to play within the media period, in microseconds. */ public final long startPositionUs; - /** - * The end position of the media to play within the media period, in microseconds, or {@link - * C#TIME_END_OF_SOURCE} if the end position is the end of the media period. - */ - public final long endPositionUs; /** * If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET} * otherwise. */ public final long contentPositionUs; /** - * The duration of the media period, like {@link #endPositionUs} but with {@link + * The duration of the media period, like {@link MediaPeriodId#endPositionUs} but with {@link * C#TIME_END_OF_SOURCE} resolved to the timeline period duration. May be {@link C#TIME_UNSET} if * the end position is not known. */ @@ -55,14 +50,12 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; MediaPeriodInfo( MediaPeriodId id, long startPositionUs, - long endPositionUs, long contentPositionUs, long durationUs, boolean isLastInTimelinePeriod, boolean isFinal) { this.id = id; this.startPositionUs = startPositionUs; - this.endPositionUs = endPositionUs; this.contentPositionUs = contentPositionUs; this.durationUs = durationUs; this.isLastInTimelinePeriod = isLastInTimelinePeriod; @@ -77,7 +70,6 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; return new MediaPeriodInfo( id.copyWithPeriodIndex(periodIndex), startPositionUs, - endPositionUs, contentPositionUs, durationUs, isLastInTimelinePeriod, @@ -89,7 +81,6 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; return new MediaPeriodInfo( id, startPositionUs, - endPositionUs, contentPositionUs, durationUs, isLastInTimelinePeriod, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java index 717f873622..e9be2d985e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java @@ -47,17 +47,18 @@ import com.google.android.exoplayer2.util.Assertions; private Timeline timeline; private @RepeatMode int repeatMode; private boolean shuffleModeEnabled; - private MediaPeriodHolder playing; - private MediaPeriodHolder reading; - private MediaPeriodHolder loading; + private @Nullable MediaPeriodHolder playing; + private @Nullable MediaPeriodHolder reading; + private @Nullable MediaPeriodHolder loading; private int length; - private Object oldFrontPeriodUid; + private @Nullable Object oldFrontPeriodUid; private long oldFrontPeriodWindowSequenceNumber; /** Creates a new media period queue. */ public MediaPeriodQueue() { period = new Timeline.Period(); window = new Timeline.Window(); + timeline = Timeline.EMPTY; } /** @@ -228,11 +229,13 @@ import com.google.android.exoplayer2.util.Assertions; reading = playing.next; } playing.release(); - playing = playing.next; length--; if (length == 0) { loading = null; + oldFrontPeriodUid = playing.uid; + oldFrontPeriodWindowSequenceNumber = playing.info.id.windowSequenceNumber; } + playing = playing.next; } else { playing = loading; reading = loading; @@ -312,7 +315,7 @@ import com.google.android.exoplayer2.util.Assertions; } else { // Check this period holder still follows the previous one, based on the new timeline. if (periodIndex == C.INDEX_UNSET - || !periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) { + || !periodHolder.uid.equals(timeline.getUidOfPeriod(periodIndex))) { // The holder uid is inconsistent with the new timeline. return !removeAfter(previousPeriodHolder); } @@ -389,7 +392,12 @@ import com.google.android.exoplayer2.util.Assertions; timeline.getPeriod(periodIndex, period); int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs); if (adGroupIndex == C.INDEX_UNSET) { - return new MediaPeriodId(periodIndex, windowSequenceNumber); + int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs); + long endPositionUs = + nextAdGroupIndex == C.INDEX_UNSET + ? C.TIME_END_OF_SOURCE + : period.getAdGroupTimeUs(nextAdGroupIndex); + return new MediaPeriodId(periodIndex, windowSequenceNumber, endPositionUs); } else { int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex); return new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup, windowSequenceNumber); @@ -448,7 +456,6 @@ import com.google.android.exoplayer2.util.Assertions; private boolean canKeepMediaPeriodHolder(MediaPeriodHolder periodHolder, MediaPeriodInfo info) { MediaPeriodInfo periodHolderInfo = periodHolder.info; return periodHolderInfo.startPositionUs == info.startPositionUs - && periodHolderInfo.endPositionUs == info.endPositionUs && periodHolderInfo.id.equals(info.id); } @@ -591,14 +598,14 @@ import com.google.android.exoplayer2.util.Assertions; mediaPeriodInfo.contentPositionUs, currentPeriodId.windowSequenceNumber); } - } else if (mediaPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE) { + } else if (mediaPeriodInfo.id.endPositionUs != C.TIME_END_OF_SOURCE) { // Play the next ad group if it's available. - int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.endPositionUs); + int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.id.endPositionUs); if (nextAdGroupIndex == C.INDEX_UNSET) { // The next ad group can't be played. Play content from the ad group position instead. return getMediaPeriodInfoForContent( currentPeriodId.periodIndex, - mediaPeriodInfo.endPositionUs, + mediaPeriodInfo.id.endPositionUs, currentPeriodId.windowSequenceNumber); } int adIndexInAdGroup = period.getFirstAdIndexToPlay(nextAdGroupIndex); @@ -608,7 +615,7 @@ import com.google.android.exoplayer2.util.Assertions; currentPeriodId.periodIndex, nextAdGroupIndex, adIndexInAdGroup, - mediaPeriodInfo.endPositionUs, + mediaPeriodInfo.id.endPositionUs, currentPeriodId.windowSequenceNumber); } else { // Check if the postroll ad should be played. @@ -637,18 +644,18 @@ import com.google.android.exoplayer2.util.Assertions; private MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info, MediaPeriodId newId) { long startPositionUs = info.startPositionUs; - long endPositionUs = info.endPositionUs; - boolean isLastInPeriod = isLastInPeriod(newId, endPositionUs); + boolean isLastInPeriod = isLastInPeriod(newId); boolean isLastInTimeline = isLastInTimeline(newId, isLastInPeriod); timeline.getPeriod(newId.periodIndex, period); long durationUs = newId.isAd() ? period.getAdDurationUs(newId.adGroupIndex, newId.adIndexInAdGroup) - : (endPositionUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endPositionUs); + : (newId.endPositionUs == C.TIME_END_OF_SOURCE + ? period.getDurationUs() + : newId.endPositionUs); return new MediaPeriodInfo( newId, startPositionUs, - endPositionUs, info.contentPositionUs, durationUs, isLastInPeriod, @@ -681,7 +688,7 @@ import com.google.android.exoplayer2.util.Assertions; long windowSequenceNumber) { MediaPeriodId id = new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup, windowSequenceNumber); - boolean isLastInPeriod = isLastInPeriod(id, C.TIME_END_OF_SOURCE); + boolean isLastInPeriod = isLastInPeriod(id); boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); long durationUs = timeline @@ -694,7 +701,6 @@ import com.google.android.exoplayer2.util.Assertions; return new MediaPeriodInfo( id, startPositionUs, - C.TIME_END_OF_SOURCE, contentPositionUs, durationUs, isLastInPeriod, @@ -703,21 +709,22 @@ import com.google.android.exoplayer2.util.Assertions; private MediaPeriodInfo getMediaPeriodInfoForContent( int periodIndex, long startPositionUs, long windowSequenceNumber) { - MediaPeriodId id = new MediaPeriodId(periodIndex, windowSequenceNumber); - timeline.getPeriod(id.periodIndex, period); int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); - long endUs = + long endPositionUs = nextAdGroupIndex == C.INDEX_UNSET ? C.TIME_END_OF_SOURCE : period.getAdGroupTimeUs(nextAdGroupIndex); - boolean isLastInPeriod = isLastInPeriod(id, endUs); + MediaPeriodId id = new MediaPeriodId(periodIndex, windowSequenceNumber, endPositionUs); + timeline.getPeriod(id.periodIndex, period); + boolean isLastInPeriod = isLastInPeriod(id); boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); - long durationUs = endUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endUs; + long durationUs = + endPositionUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endPositionUs; return new MediaPeriodInfo( - id, startPositionUs, endUs, C.TIME_UNSET, durationUs, isLastInPeriod, isLastInTimeline); + id, startPositionUs, C.TIME_UNSET, durationUs, isLastInPeriod, isLastInTimeline); } - private boolean isLastInPeriod(MediaPeriodId id, long endPositionUs) { + private boolean isLastInPeriod(MediaPeriodId id) { int adGroupCount = timeline.getPeriod(id.periodIndex, period).getAdGroupCount(); if (adGroupCount == 0) { return true; @@ -727,7 +734,7 @@ import com.google.android.exoplayer2.util.Assertions; boolean isAd = id.isAd(); if (period.getAdGroupTimeUs(lastAdGroupIndex) != C.TIME_END_OF_SOURCE) { // There's no postroll ad. - return !isAd && endPositionUs == C.TIME_END_OF_SOURCE; + return !isAd && id.endPositionUs == C.TIME_END_OF_SOURCE; } int postrollAdCount = period.getAdCountInAdGroup(lastAdGroupIndex); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index 80de073e2d..b338de15b4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -25,34 +25,81 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; */ /* package */ final class PlaybackInfo { + /** + * Dummy media period id used while the timeline is empty and no period id is specified. This id + * is used when playback infos are created with {@link #createDummy(long, TrackSelectorResult)}. + */ + public static final MediaPeriodId DUMMY_MEDIA_PERIOD_ID = new MediaPeriodId(/* periodIndex= */ 0); + + /** The current {@link Timeline}. */ public final Timeline timeline; + /** The current manifest. */ public final @Nullable Object manifest; + /** The {@link MediaPeriodId} of the currently playing media period in the {@link #timeline}. */ public final MediaPeriodId periodId; + /** + * The start position at which playback started in {@link #periodId} relative to the start of the + * associated period in the {@link #timeline}, in microseconds. + */ public final long startPositionUs; + /** + * If {@link #periodId} refers to an ad, the position of the suspended content relative to the + * start of the associated period in the {@link #timeline}, in microseconds. {@link C#TIME_UNSET} + * if {@link #periodId} does not refer to an ad. + */ public final long contentPositionUs; + /** The current playback state. One of the {@link Player}.STATE_ constants. */ public final int playbackState; + /** Whether the player is currently loading. */ public final boolean isLoading; + /** The currently available track groups. */ public final TrackGroupArray trackGroups; + /** The result of the current track selection. */ public final TrackSelectorResult trackSelectorResult; + /** The {@link MediaPeriodId} of the currently loading media period in the {@link #timeline}. */ + public final MediaPeriodId loadingMediaPeriodId; - public volatile long positionUs; + /** + * Position up to which media is buffered in {@link #loadingMediaPeriodId) relative to the start + * of the associated period in the {@link #timeline}, in microseconds. + */ public volatile long bufferedPositionUs; + /** + * Total duration of buffered media from {@link #positionUs} to {@link #bufferedPositionUs} + * including all ads. + */ + public volatile long totalBufferedDurationUs; + /** + * Current playback position in {@link #periodId} relative to the start of the associated period + * in the {@link #timeline}, in microseconds. + */ + public volatile long positionUs; - public PlaybackInfo( - Timeline timeline, - long startPositionUs, - TrackGroupArray trackGroups, - TrackSelectorResult trackSelectorResult) { - this( - timeline, + /** + * Creates empty dummy playback info which can be used for masking as long as no real playback + * info is available. + * + * @param startPositionUs The start position at which playback should start, in microseconds. + * @param emptyTrackSelectorResult An empty track selector result with null entries for each + * renderer. + * @return A dummy playback info. + */ + public static PlaybackInfo createDummy( + long startPositionUs, TrackSelectorResult emptyTrackSelectorResult) { + return new PlaybackInfo( + Timeline.EMPTY, /* manifest= */ null, - new MediaPeriodId(/* periodIndex= */ 0), + DUMMY_MEDIA_PERIOD_ID, startPositionUs, - /* contentPositionUs =*/ C.TIME_UNSET, + /* contentPositionUs= */ C.TIME_UNSET, Player.STATE_IDLE, /* isLoading= */ false, - trackGroups, - trackSelectorResult); + TrackGroupArray.EMPTY, + emptyTrackSelectorResult, + DUMMY_MEDIA_PERIOD_ID, + startPositionUs, + /* totalBufferedDurationUs= */ 0, + startPositionUs); } public PlaybackInfo( @@ -64,18 +111,24 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; int playbackState, boolean isLoading, TrackGroupArray trackGroups, - TrackSelectorResult trackSelectorResult) { + TrackSelectorResult trackSelectorResult, + MediaPeriodId loadingMediaPeriodId, + long bufferedPositionUs, + long totalBufferedDurationUs, + long positionUs) { this.timeline = timeline; this.manifest = manifest; this.periodId = periodId; this.startPositionUs = startPositionUs; this.contentPositionUs = contentPositionUs; - this.positionUs = startPositionUs; - this.bufferedPositionUs = startPositionUs; this.playbackState = playbackState; this.isLoading = isLoading; this.trackGroups = trackGroups; this.trackSelectorResult = trackSelectorResult; + this.loadingMediaPeriodId = loadingMediaPeriodId; + this.bufferedPositionUs = bufferedPositionUs; + this.totalBufferedDurationUs = totalBufferedDurationUs; + this.positionUs = positionUs; } public PlaybackInfo fromNewPosition( @@ -89,93 +142,113 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; playbackState, isLoading, trackGroups, - trackSelectorResult); + trackSelectorResult, + periodId, + startPositionUs, + /* totalBufferedDurationUs= */ 0, + startPositionUs); } public PlaybackInfo copyWithPeriodIndex(int periodIndex) { - PlaybackInfo playbackInfo = - new PlaybackInfo( - timeline, - manifest, - periodId.copyWithPeriodIndex(periodIndex), - startPositionUs, - contentPositionUs, - playbackState, - isLoading, - trackGroups, - trackSelectorResult); - copyMutablePositions(this, playbackInfo); - return playbackInfo; + return new PlaybackInfo( + timeline, + manifest, + periodId.copyWithPeriodIndex(periodIndex), + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); } public PlaybackInfo copyWithTimeline(Timeline timeline, Object manifest) { - PlaybackInfo playbackInfo = - new PlaybackInfo( - timeline, - manifest, - periodId, - startPositionUs, - contentPositionUs, - playbackState, - isLoading, - trackGroups, - trackSelectorResult); - copyMutablePositions(this, playbackInfo); - return playbackInfo; + return new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); } public PlaybackInfo copyWithPlaybackState(int playbackState) { - PlaybackInfo playbackInfo = - new PlaybackInfo( - timeline, - manifest, - periodId, - startPositionUs, - contentPositionUs, - playbackState, - isLoading, - trackGroups, - trackSelectorResult); - copyMutablePositions(this, playbackInfo); - return playbackInfo; + return new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); } public PlaybackInfo copyWithIsLoading(boolean isLoading) { - PlaybackInfo playbackInfo = - new PlaybackInfo( - timeline, - manifest, - periodId, - startPositionUs, - contentPositionUs, - playbackState, - isLoading, - trackGroups, - trackSelectorResult); - copyMutablePositions(this, playbackInfo); - return playbackInfo; + return new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); } public PlaybackInfo copyWithTrackInfo( TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) { - PlaybackInfo playbackInfo = - new PlaybackInfo( - timeline, - manifest, - periodId, - startPositionUs, - contentPositionUs, - playbackState, - isLoading, - trackGroups, - trackSelectorResult); - copyMutablePositions(this, playbackInfo); - return playbackInfo; + return new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); } - private static void copyMutablePositions(PlaybackInfo from, PlaybackInfo to) { - to.positionUs = from.positionUs; - to.bufferedPositionUs = from.bufferedPositionUs; + public PlaybackInfo copyWithLoadingMediaPeriodId(MediaPeriodId loadingMediaPeriodId) { + return new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 328816d709..87aec0c253 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -22,9 +22,13 @@ import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; +import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.audio.AudioListener; +import com.google.android.exoplayer2.audio.AuxEffectInfo; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoListener; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -50,6 +54,89 @@ import java.lang.annotation.RetentionPolicy; */ public interface Player { + /** The audio component of a {@link Player}. */ + interface AudioComponent { + + /** + * Adds a listener to receive audio events. + * + * @param listener The listener to register. + */ + void addAudioListener(AudioListener listener); + + /** + * Removes a listener of audio events. + * + * @param listener The listener to unregister. + */ + void removeAudioListener(AudioListener listener); + + /** + * Sets the attributes for audio playback, used by the underlying audio track. If not set, the + * default audio attributes will be used. They are suitable for general media playback. + * + *

    Setting the audio attributes during playback may introduce a short gap in audio output as + * the audio track is recreated. A new audio session id will also be generated. + * + *

    If tunneling is enabled by the track selector, the specified audio attributes will be + * ignored, but they will take effect if audio is later played without tunneling. + * + *

    If the device is running a build before platform API version 21, audio attributes cannot + * be set directly on the underlying audio track. In this case, the usage will be mapped onto an + * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}. + * + * @param audioAttributes The attributes to use for audio playback. + * @deprecated Use {@link AudioComponent#setAudioAttributes(AudioAttributes, boolean)}. + */ + @Deprecated + void setAudioAttributes(AudioAttributes audioAttributes); + + /** + * Sets the attributes for audio playback, used by the underlying audio track. If not set, the + * default audio attributes will be used. They are suitable for general media playback. + * + *

    Setting the audio attributes during playback may introduce a short gap in audio output as + * the audio track is recreated. A new audio session id will also be generated. + * + *

    If tunneling is enabled by the track selector, the specified audio attributes will be + * ignored, but they will take effect if audio is later played without tunneling. + * + *

    If the device is running a build before platform API version 21, audio attributes cannot + * be set directly on the underlying audio track. In this case, the usage will be mapped onto an + * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}. + * + *

    If audio focus should be handled, the {@link AudioAttributes#usage} must be {@link + * C#USAGE_MEDIA} or {@link C#USAGE_GAME}. Other usages will throw an {@link + * IllegalArgumentException}. + * + * @param audioAttributes The attributes to use for audio playback. + * @param handleAudioFocus True if the player should handle audio focus, false otherwise. + */ + void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus); + + /** Returns the attributes for audio playback. */ + AudioAttributes getAudioAttributes(); + + /** Returns the audio session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} if not set. */ + int getAudioSessionId(); + + /** Sets information on an auxiliary audio effect to attach to the underlying audio track. */ + void setAuxEffectInfo(AuxEffectInfo auxEffectInfo); + + /** Detaches any previously attached auxiliary audio effect from the underlying audio track. */ + void clearAuxEffectInfo(); + + /** + * Sets the audio volume, with 0 being silence and 1 being unity gain. + * + * @param audioVolume The audio volume. + */ + void setVolume(float audioVolume); + + /** Returns the audio volume, with 0 being silence and 1 being unity gain. */ + float getVolume(); + } + /** The video component of a {@link Player}. */ interface VideoComponent { @@ -97,7 +184,7 @@ public interface Player { * * @param surface The {@link Surface}. */ - void setVideoSurface(Surface surface); + void setVideoSurface(@Nullable Surface surface); /** * Clears the {@link Surface} onto which video is being rendered if it matches the one passed. @@ -175,23 +262,25 @@ public interface Player { } /** - * Listener of changes in player state. + * Listener of changes in player state. All methods have no-op default implementations to allow + * selective overrides. */ interface EventListener { /** * Called when the timeline and/or manifest has been refreshed. - *

    - * Note that if the timeline has changed then a position discontinuity may also have occurred. - * For example, the current period index may have changed as a result of periods being added or - * removed from the timeline. This will not be reported via a separate call to + * + *

    Note that if the timeline has changed then a position discontinuity may also have + * occurred. For example, the current period index may have changed as a result of periods being + * added or removed from the timeline. This will not be reported via a separate call to * {@link #onPositionDiscontinuity(int)}. * * @param timeline The latest timeline. Never null, but may be empty. * @param manifest The latest manifest. May be null. * @param reason The {@link TimelineChangeReason} responsible for this timeline change. */ - void onTimelineChanged(Timeline timeline, Object manifest, @TimelineChangeReason int reason); + default void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {} /** * Called when the available or selected tracks change. @@ -200,46 +289,47 @@ public interface Player { * @param trackSelections The track selections for each renderer. Never null and always of * length {@link #getRendererCount()}, but may contain null elements. */ - void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections); + default void onTracksChanged( + TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {} /** * Called when the player starts or stops loading the source. * * @param isLoading Whether the source is currently being loaded. */ - void onLoadingChanged(boolean isLoading); + default void onLoadingChanged(boolean isLoading) {} /** - * Called when the value returned from either {@link #getPlayWhenReady()} or - * {@link #getPlaybackState()} changes. + * Called when the value returned from either {@link #getPlayWhenReady()} or {@link + * #getPlaybackState()} changes. * * @param playWhenReady Whether playback will proceed when ready. * @param playbackState One of the {@code STATE} constants. */ - void onPlayerStateChanged(boolean playWhenReady, int playbackState); + default void onPlayerStateChanged(boolean playWhenReady, int playbackState) {} /** * Called when the value of {@link #getRepeatMode()} changes. * * @param repeatMode The {@link RepeatMode} used for playback. */ - void onRepeatModeChanged(@RepeatMode int repeatMode); + default void onRepeatModeChanged(@RepeatMode int repeatMode) {} /** * Called when the value of {@link #getShuffleModeEnabled()} changes. * * @param shuffleModeEnabled Whether shuffling of windows is enabled. */ - void onShuffleModeEnabledChanged(boolean shuffleModeEnabled); + default void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {} /** * Called when an error occurs. The playback state will transition to {@link #STATE_IDLE} - * immediately after this method is called. The player instance can still be used, and - * {@link #release()} must still be called on the player should it no longer be required. + * immediately after this method is called. The player instance can still be used, and {@link + * #release()} must still be called on the player should it no longer be required. * * @param error The error. */ - void onPlayerError(ExoPlaybackException error); + default void onPlayerError(ExoPlaybackException error) {} /** * Called when a position discontinuity occurs without a change to the timeline. A position @@ -247,14 +337,14 @@ public interface Player { * transitioning from one period in the timeline to the next), or when the playback position * jumps within the period currently being played (as a result of a seek being performed, or * when the source introduces a discontinuity internally). - *

    - * When a position discontinuity occurs as a result of a change to the timeline this method is - * not called. {@link #onTimelineChanged(Timeline, Object, int)} is called in this + * + *

    When a position discontinuity occurs as a result of a change to the timeline this method + * is not called. {@link #onTimelineChanged(Timeline, Object, int)} is called in this * case. * * @param reason The {@link DiscontinuityReason} responsible for the discontinuity. */ - void onPositionDiscontinuity(@DiscontinuityReason int reason); + default void onPositionDiscontinuity(@DiscontinuityReason int reason) {} /** * Called when the current playback parameters change. The playback parameters may change due to @@ -264,83 +354,35 @@ public interface Player { * * @param playbackParameters The playback parameters. */ - void onPlaybackParametersChanged(PlaybackParameters playbackParameters); + default void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {} /** * Called when all pending seek requests have been processed by the player. This is guaranteed - * to happen after any necessary changes to the player state were reported to - * {@link #onPlayerStateChanged(boolean, int)}. + * to happen after any necessary changes to the player state were reported to {@link + * #onPlayerStateChanged(boolean, int)}. */ - void onSeekProcessed(); - + default void onSeekProcessed() {} } /** - * {@link EventListener} allowing selective overrides. All methods are implemented as no-ops. + * @deprecated Use {@link EventListener} interface directly for selective overrides as all methods + * are implemented as no-op default methods. */ + @Deprecated abstract class DefaultEventListener implements EventListener { @Override - public void onTimelineChanged(Timeline timeline, Object manifest, - @TimelineChangeReason int reason) { + public void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { // Call deprecated version. Otherwise, do nothing. onTimelineChanged(timeline, manifest); } - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - // Do nothing. - } - - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - // Do nothing. - } - - @Override - public void onRepeatModeChanged(@RepeatMode int repeatMode) { - // Do nothing. - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - // Do nothing. - } - - @Override - public void onPlayerError(ExoPlaybackException error) { - // Do nothing. - } - - @Override - public void onPositionDiscontinuity(@DiscontinuityReason int reason) { - // Do nothing. - } - - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - - @Override - public void onSeekProcessed() { - // Do nothing. - } - - /** - * @deprecated Use {@link DefaultEventListener#onTimelineChanged(Timeline, Object, int)} - * instead. - */ + /** @deprecated Use {@link EventListener#onTimelineChanged(Timeline, Object, int)} instead. */ @Deprecated - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, @Nullable Object manifest) { // Do nothing. } - } /** @@ -428,6 +470,10 @@ public interface Player { */ int TIMELINE_CHANGE_REASON_DYNAMIC = 2; + /** Returns the component of this player for audio output, or null if audio is not supported. */ + @Nullable + AudioComponent getAudioComponent(); + /** Returns the component of this player for video output, or null if video is not supported. */ @Nullable VideoComponent getVideoComponent(); @@ -678,23 +724,27 @@ public interface Player { */ long getDuration(); - /** - * Returns the playback position in the current window, in milliseconds. - */ + /** Returns the playback position in the current content window or ad, in milliseconds. */ long getCurrentPosition(); /** - * Returns an estimate of the position in the current window up to which data is buffered, in - * milliseconds. + * Returns an estimate of the position in the current content window or ad up to which data is + * buffered, in milliseconds. */ long getBufferedPosition(); /** - * Returns an estimate of the percentage in the current window up to which data is buffered, or 0 - * if no estimate is available. + * Returns an estimate of the percentage in the current content window or ad up to which data is + * buffered, or 0 if no estimate is available. */ int getBufferedPercentage(); + /** + * Returns an estimate of the total buffered duration from the current position, in milliseconds. + * This includes pre-buffered data for subsequent ads and windows. + */ + long getTotalBufferedDuration(); + /** * Returns whether the current window is dynamic, or {@code false} if the {@link Timeline} is * empty. @@ -735,4 +785,10 @@ public interface Player { */ long getContentPosition(); + /** + * If {@link #isPlayingAd()} returns {@code true}, returns an estimate of the content position in + * the current content window up to which data is buffered, in milliseconds. If there is no ad + * playing, the returned position is the same as that returned by {@link #getBufferedPosition()}. + */ + long getContentBufferedPosition(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java index e53db4568d..c29017856f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -192,6 +192,18 @@ public interface Renderer extends PlayerMessage.Target { */ void resetPosition(long positionUs) throws ExoPlaybackException; + /** + * Sets the operating rate of this renderer, where 1 is the default rate, 2 is twice the default + * rate, 0.5 is half the default rate and so on. The operating rate is a hint to the renderer of + * the speed at which playback will proceed, and may be used for resource planning. + * + *

    The default implementation is a no-op. + * + * @param operatingRate The operating rate. + * @throws ExoPlaybackException If an error occurs handling the operating rate. + */ + default void setOperatingRate(float operatingRate) throws ExoPlaybackException {}; + /** * Incrementally renders the {@link SampleStream}. *

    diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 482e2c970a..055cf1de17 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer2; import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.media.MediaCodec; import android.media.PlaybackParams; @@ -30,7 +32,10 @@ import android.view.TextureView; import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.audio.AudioFocusManager; +import com.google.android.exoplayer2.audio.AudioListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.AuxEffectInfo; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -43,6 +48,7 @@ import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; @@ -56,7 +62,8 @@ import java.util.concurrent.CopyOnWriteArraySet; * be obtained from {@link ExoPlayerFactory}. */ @TargetApi(16) -public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player.TextComponent { +public class SimpleExoPlayer + implements ExoPlayer, Player.AudioComponent, Player.VideoComponent, Player.TextComponent { /** @deprecated Use {@link com.google.android.exoplayer2.video.VideoListener}. */ @Deprecated @@ -66,103 +73,170 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player protected final Renderer[] renderers; - private final ExoPlayer player; + private final ExoPlayerImpl player; private final Handler eventHandler; private final ComponentListener componentListener; private final CopyOnWriteArraySet videoListeners; + private final CopyOnWriteArraySet audioListeners; private final CopyOnWriteArraySet textOutputs; private final CopyOnWriteArraySet metadataOutputs; private final CopyOnWriteArraySet videoDebugListeners; private final CopyOnWriteArraySet audioDebugListeners; + private final BandwidthMeter bandwidthMeter; private final AnalyticsCollector analyticsCollector; + private final AudioFocusManager audioFocusManager; + private Format videoFormat; private Format audioFormat; private Surface surface; private boolean ownsSurface; - @C.VideoScalingMode - private int videoScalingMode; + private @C.VideoScalingMode int videoScalingMode; private SurfaceHolder surfaceHolder; private TextureView textureView; + private int surfaceWidth; + private int surfaceHeight; private DecoderCounters videoDecoderCounters; private DecoderCounters audioDecoderCounters; private int audioSessionId; private AudioAttributes audioAttributes; private float audioVolume; private MediaSource mediaSource; + private List currentCues; /** * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. + * @param looper The {@link Looper} which must be used for all calls to the player and which is + * used to call listeners on. + * @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl, + * BandwidthMeter, DrmSessionManager, Looper)}. The use of {@link + * SimpleExoPlayer#setAudioAttributes(AudioAttributes, boolean)} to manage audio focus will be + * unavailable for a player created with this constructor. */ + @Deprecated protected SimpleExoPlayer( RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, - @Nullable DrmSessionManager drmSessionManager) { + BandwidthMeter bandwidthMeter, + @Nullable DrmSessionManager drmSessionManager, + Looper looper) { this( + /* context= */ null, renderersFactory, trackSelector, loadControl, drmSessionManager, - new AnalyticsCollector.Factory()); + bandwidthMeter, + new AnalyticsCollector.Factory(), + looper); } /** + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance + * will not be used for DRM protected playbacks. + * @param looper The {@link Looper} which must be used for all calls to the player and which is + * used to call listeners on. + */ + protected SimpleExoPlayer( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + @Nullable DrmSessionManager drmSessionManager, + Looper looper) { + this( + context, + renderersFactory, + trackSelector, + loadControl, + drmSessionManager, + bandwidthMeter, + new AnalyticsCollector.Factory(), + looper); + } + + /** + * @param context A {@link Context}. * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. + * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that * will collect and forward all player events. + * @param looper The {@link Looper} which must be used for all calls to the player and which is + * used to call listeners on. */ protected SimpleExoPlayer( + Context context, RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, - AnalyticsCollector.Factory analyticsCollectorFactory) { + BandwidthMeter bandwidthMeter, + AnalyticsCollector.Factory analyticsCollectorFactory, + Looper looper) { this( + context, renderersFactory, trackSelector, loadControl, drmSessionManager, + bandwidthMeter, analyticsCollectorFactory, - Clock.DEFAULT); + Clock.DEFAULT, + looper); } /** + * @param context A {@link Context}. * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. + * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that * will collect and forward all player events. * @param clock The {@link Clock} that will be used by the instance. Should always be {@link * Clock#DEFAULT}, unless the player is being used from a test. + * @param looper The {@link Looper} which must be used for all calls to the player and which is + * used to call listeners on. */ protected SimpleExoPlayer( + Context context, RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, + BandwidthMeter bandwidthMeter, AnalyticsCollector.Factory analyticsCollectorFactory, - Clock clock) { + Clock clock, + Looper looper) { + this.bandwidthMeter = bandwidthMeter; componentListener = new ComponentListener(); videoListeners = new CopyOnWriteArraySet<>(); + audioListeners = new CopyOnWriteArraySet<>(); textOutputs = new CopyOnWriteArraySet<>(); metadataOutputs = new CopyOnWriteArraySet<>(); videoDebugListeners = new CopyOnWriteArraySet<>(); audioDebugListeners = new CopyOnWriteArraySet<>(); - Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); - eventHandler = new Handler(eventLooper); + eventHandler = new Handler(looper); renderers = renderersFactory.createRenderers( eventHandler, @@ -177,17 +251,28 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player audioSessionId = C.AUDIO_SESSION_ID_UNSET; audioAttributes = AudioAttributes.DEFAULT; videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; + currentCues = Collections.emptyList(); // Build the player and associated objects. - player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock); + player = + new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); analyticsCollector = analyticsCollectorFactory.createAnalyticsCollector(player, clock); addListener(analyticsCollector); videoDebugListeners.add(analyticsCollector); + videoListeners.add(analyticsCollector); audioDebugListeners.add(analyticsCollector); + audioListeners.add(analyticsCollector); addMetadataOutput(analyticsCollector); + bandwidthMeter.addEventListener(eventHandler, analyticsCollector); if (drmSessionManager instanceof DefaultDrmSessionManager) { ((DefaultDrmSessionManager) drmSessionManager).addListener(eventHandler, analyticsCollector); } + audioFocusManager = new AudioFocusManager(context, componentListener); + } + + @Override + public AudioComponent getAudioComponent() { + return this; } @Override @@ -236,6 +321,8 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player public void setVideoSurface(Surface surface) { removeSurfaceCallbacks(); setVideoSurfaceInternal(surface, false); + int newSurfaceSize = surface == null ? 0 : C.LENGTH_UNSET; + maybeNotifySurfaceSizeChanged(/* width= */ newSurfaceSize, /* height= */ newSurfaceSize); } @Override @@ -251,10 +338,18 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player this.surfaceHolder = surfaceHolder; if (surfaceHolder == null) { setVideoSurfaceInternal(null, false); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } else { surfaceHolder.addCallback(componentListener); Surface surface = surfaceHolder.getSurface(); - setVideoSurfaceInternal(surface != null && surface.isValid() ? surface : null, false); + if (surface != null && surface.isValid()) { + setVideoSurfaceInternal(surface, /* ownsSurface= */ false); + Rect surfaceSize = surfaceHolder.getSurfaceFrame(); + maybeNotifySurfaceSizeChanged(surfaceSize.width(), surfaceSize.height()); + } else { + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); + } } } @@ -281,6 +376,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player this.textureView = textureView; if (textureView == null) { setVideoSurfaceInternal(null, true); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } else { if (textureView.getSurfaceTextureListener() != null) { Log.w(TAG, "Replacing existing SurfaceTextureListener."); @@ -288,7 +384,13 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player textureView.setSurfaceTextureListener(componentListener); SurfaceTexture surfaceTexture = textureView.isAvailable() ? textureView.getSurfaceTexture() : null; - setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true); + if (surfaceTexture == null) { + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); + } else { + setVideoSurfaceInternal(new Surface(surfaceTexture), /* ownsSurface= */ true); + maybeNotifySurfaceSizeChanged(textureView.getWidth(), textureView.getHeight()); + } } } @@ -299,6 +401,92 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player } } + @Override + public void addAudioListener(AudioListener listener) { + audioListeners.add(listener); + } + + @Override + public void removeAudioListener(AudioListener listener) { + audioListeners.remove(listener); + } + + @Override + public void setAudioAttributes(AudioAttributes audioAttributes) { + setAudioAttributes(audioAttributes, /* handleAudioFocus= */ false); + } + + @Override + public void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus) { + if (!Util.areEqual(this.audioAttributes, audioAttributes)) { + this.audioAttributes = audioAttributes; + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_AUDIO_ATTRIBUTES) + .setPayload(audioAttributes) + .send(); + } + } + for (AudioListener audioListener : audioListeners) { + audioListener.onAudioAttributesChanged(audioAttributes); + } + } + + @AudioFocusManager.PlayerCommand + int playerCommand = + audioFocusManager.setAudioAttributes( + handleAudioFocus ? audioAttributes : null, getPlayWhenReady(), getPlaybackState()); + updatePlayWhenReady(getPlayWhenReady(), playerCommand); + } + + @Override + public AudioAttributes getAudioAttributes() { + return audioAttributes; + } + + @Override + public int getAudioSessionId() { + return audioSessionId; + } + + @Override + public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) { + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_AUX_EFFECT_INFO) + .setPayload(auxEffectInfo) + .send(); + } + } + } + + @Override + public void clearAuxEffectInfo() { + setAuxEffectInfo(new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, /* sendLevel= */ 0f)); + } + + @Override + public void setVolume(float audioVolume) { + audioVolume = Util.constrainValue(audioVolume, /* min= */ 0, /* max= */ 1); + if (this.audioVolume == audioVolume) { + return; + } + this.audioVolume = audioVolume; + sendVolumeToRenderers(); + for (AudioListener audioListener : audioListeners) { + audioListener.onVolumeChanged(audioVolume); + } + } + + @Override + public float getVolume() { + return audioVolume; + } + /** * Sets the stream type for audio playback, used by the underlying audio track. *

    @@ -353,63 +541,6 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player analyticsCollector.removeListener(listener); } - /** - * Sets the attributes for audio playback, used by the underlying audio track. If not set, the - * default audio attributes will be used. They are suitable for general media playback. - *

    - * Setting the audio attributes during playback may introduce a short gap in audio output as the - * audio track is recreated. A new audio session id will also be generated. - *

    - * If tunneling is enabled by the track selector, the specified audio attributes will be ignored, - * but they will take effect if audio is later played without tunneling. - *

    - * If the device is running a build before platform API version 21, audio attributes cannot be set - * directly on the underlying audio track. In this case, the usage will be mapped onto an - * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}. - * - * @param audioAttributes The attributes to use for audio playback. - */ - public void setAudioAttributes(AudioAttributes audioAttributes) { - this.audioAttributes = audioAttributes; - for (Renderer renderer : renderers) { - if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - player - .createMessage(renderer) - .setType(C.MSG_SET_AUDIO_ATTRIBUTES) - .setPayload(audioAttributes) - .send(); - } - } - } - - /** - * Returns the attributes for audio playback. - */ - public AudioAttributes getAudioAttributes() { - return audioAttributes; - } - - /** - * Sets the audio volume, with 0 being silence and 1 being unity gain. - * - * @param audioVolume The audio volume. - */ - public void setVolume(float audioVolume) { - this.audioVolume = audioVolume; - for (Renderer renderer : renderers) { - if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setPayload(audioVolume).send(); - } - } - } - - /** - * Returns the audio volume, with 0 being silence and 1 being unity gain. - */ - public float getVolume() { - return audioVolume; - } - /** * Sets the {@link PlaybackParams} governing audio playback. * @@ -443,13 +574,6 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player return audioFormat; } - /** - * Returns the audio session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} if not set. - */ - public int getAudioSessionId() { - return audioSessionId; - } - /** * Returns {@link DecoderCounters} for video, or null if no video is being played. */ @@ -502,6 +626,9 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player @Override public void addTextOutput(TextOutput listener) { + if (!currentCues.isEmpty()) { + listener.onCues(currentCues); + } textOutputs.add(listener); } @@ -645,6 +772,11 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player return player.getPlaybackLooper(); } + @Override + public Looper getApplicationLooper() { + return player.getApplicationLooper(); + } + @Override public void addListener(Player.EventListener listener) { player.addListener(listener); @@ -680,12 +812,17 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player mediaSource.addEventListener(eventHandler, analyticsCollector); this.mediaSource = mediaSource; } + @AudioFocusManager.PlayerCommand + int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); + updatePlayWhenReady(getPlayWhenReady(), playerCommand); player.prepare(mediaSource, resetPosition, resetState); } @Override public void setPlayWhenReady(boolean playWhenReady) { - player.setPlayWhenReady(playWhenReady); + @AudioFocusManager.PlayerCommand + int playerCommand = audioFocusManager.handleSetPlayWhenReady(playWhenReady, getPlaybackState()); + updatePlayWhenReady(playWhenReady, playerCommand); } @Override @@ -757,6 +894,11 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player player.setSeekParameters(seekParameters); } + @Override + public SeekParameters getSeekParameters() { + return player.getSeekParameters(); + } + @Override public @Nullable Object getCurrentTag() { return player.getCurrentTag(); @@ -775,10 +917,13 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player mediaSource = null; analyticsCollector.resetForNewMediaSource(); } + audioFocusManager.handleStop(); + currentCues = Collections.emptyList(); } @Override public void release() { + audioFocusManager.handleStop(); player.release(); removeSurfaceCallbacks(); if (surface != null) { @@ -790,6 +935,8 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player if (mediaSource != null) { mediaSource.removeEventListener(analyticsCollector); } + bandwidthMeter.removeEventListener(analyticsCollector); + currentCues = Collections.emptyList(); } @Override @@ -877,6 +1024,11 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player return player.getBufferedPercentage(); } + @Override + public long getTotalBufferedDuration() { + return player.getTotalBufferedDuration(); + } + @Override public boolean isCurrentWindowDynamic() { return player.isCurrentWindowDynamic(); @@ -907,22 +1059,13 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player return player.getContentPosition(); } - // Internal methods. - - /** - * Creates the {@link ExoPlayer} implementation used by this instance. - * - * @param renderers The {@link Renderer}s that will be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param clock The {@link Clock} that will be used by this instance. - * @return A new {@link ExoPlayer} instance. - */ - protected ExoPlayer createExoPlayerImpl( - Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) { - return new ExoPlayerImpl(renderers, trackSelector, loadControl, clock); + @Override + public long getContentBufferedPosition() { + return player.getContentBufferedPosition(); } + // Internal methods. + private void removeSurfaceCallbacks() { if (textureView != null) { if (textureView.getSurfaceTextureListener() != componentListener) { @@ -966,9 +1109,41 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player this.ownsSurface = ownsSurface; } - private final class ComponentListener implements VideoRendererEventListener, - AudioRendererEventListener, TextOutput, MetadataOutput, SurfaceHolder.Callback, - TextureView.SurfaceTextureListener { + private void maybeNotifySurfaceSizeChanged(int width, int height) { + if (width != surfaceWidth || height != surfaceHeight) { + surfaceWidth = width; + surfaceHeight = height; + for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) { + videoListener.onSurfaceSizeChanged(width, height); + } + } + } + + private void sendVolumeToRenderers() { + float scaledVolume = audioVolume * audioFocusManager.getVolumeMultiplier(); + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { + player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setPayload(scaledVolume).send(); + } + } + } + + private void updatePlayWhenReady( + boolean playWhenReady, @AudioFocusManager.PlayerCommand int playerCommand) { + + player.setPlayWhenReady( + playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY, + playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY); + } + + private final class ComponentListener + implements VideoRendererEventListener, + AudioRendererEventListener, + TextOutput, + MetadataOutput, + SurfaceHolder.Callback, + TextureView.SurfaceTextureListener, + AudioFocusManager.PlayerControl { // VideoRendererEventListener implementation @@ -1008,8 +1183,12 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) { - videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, - pixelWidthHeightRatio); + // Prevent duplicate notification if a listener is both a VideoRendererEventListener and + // a VideoListener, as they have the same method signature. + if (!videoDebugListeners.contains(videoListener)) { + videoListener.onVideoSizeChanged( + width, height, unappliedRotationDegrees, pixelWidthHeightRatio); + } } for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, @@ -1050,7 +1229,17 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player @Override public void onAudioSessionId(int sessionId) { + if (audioSessionId == sessionId) { + return; + } audioSessionId = sessionId; + for (AudioListener audioListener : audioListeners) { + // Prevent duplicate notification if a listener is both a AudioRendererEventListener and + // a AudioListener, as they have the same method signature. + if (!audioDebugListeners.contains(audioListener)) { + audioListener.onAudioSessionId(sessionId); + } + } for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioSessionId(sessionId); } @@ -1095,6 +1284,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player @Override public void onCues(List cues) { + currentCues = cues; for (TextOutput textOutput : textOutputs) { textOutput.onCues(cues); } @@ -1118,12 +1308,13 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - // Do nothing. + maybeNotifySurfaceSizeChanged(width, height); } @Override public void surfaceDestroyed(SurfaceHolder holder) { setVideoSurfaceInternal(null, false); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } // TextureView.SurfaceTextureListener implementation @@ -1131,16 +1322,18 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player @Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { setVideoSurfaceInternal(new Surface(surfaceTexture), true); + maybeNotifySurfaceSizeChanged(width, height); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) { - // Do nothing. + maybeNotifySurfaceSizeChanged(width, height); } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { setVideoSurfaceInternal(null, true); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); return true; } @@ -1149,6 +1342,16 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player // Do nothing. } - } + // AudioFocusManager.PlayerControl implementation + @Override + public void setVolumeMultiplier(float volumeMultiplier) { + sendVolumeToRenderers(); + } + + @Override + public void executePlayerCommand(@AudioFocusManager.PlayerCommand int playerCommand) { + updatePlayWhenReady(getPlayWhenReady(), playerCommand); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 600fbc3014..a1a0e9b152 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -520,6 +520,11 @@ public abstract class Timeline { public int getIndexOfPeriod(Object uid) { return C.INDEX_UNSET; } + + @Override + public Object getUidOfPeriod(int periodIndex) { + throw new IndexOutOfBoundsException(); + } }; /** @@ -737,6 +742,17 @@ public abstract class Timeline { return Pair.create(periodIndex, periodPositionUs); } + /** + * Populates a {@link Period} with data for the period with the specified unique identifier. + * + * @param periodUid The unique identifier of the period. + * @param period The {@link Period} to populate. Must not be null. + * @return The populated {@link Period}, for convenience. + */ + public Period getPeriodByUid(Object periodUid, Period period) { + return getPeriod(getIndexOfPeriod(periodUid), period, /* setIds= */ true); + } + /** * Populates a {@link Period} with data for the period at the specified index. Does not populate * {@link Period#id} and {@link Period#uid}. @@ -770,4 +786,11 @@ public abstract class Timeline { */ public abstract int getIndexOfPeriod(Object uid); + /** + * Returns the unique id of the period identified by its index in the timeline. + * + * @param periodIndex The index of the period. + * @return The unique id of the period. + */ + public abstract Object getUidOfPeriod(int periodIndex); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index 43ef308f27..262187586b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.analytics; -import android.net.NetworkInfo; import android.support.annotation.Nullable; import android.view.Surface; import com.google.android.exoplayer2.C; @@ -27,6 +26,8 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; +import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.audio.AudioListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener; @@ -39,6 +40,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.video.VideoListener; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.io.IOException; import java.util.ArrayList; @@ -46,6 +48,7 @@ import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Data collector which is able to forward analytics events to {@link AnalyticsListener}s by @@ -58,7 +61,9 @@ public class AnalyticsCollector VideoRendererEventListener, MediaSourceEventListener, BandwidthMeter.EventListener, - DefaultDrmSessionEventListener { + DefaultDrmSessionEventListener, + VideoListener, + AudioListener { /** Factory for an analytics collector. */ public static class Factory { @@ -66,29 +71,34 @@ public class AnalyticsCollector /** * Creates an analytics collector for the specified player. * - * @param player The {@link Player} for which data will be collected. + * @param player The {@link Player} for which data will be collected. Can be null, if the player + * is set by calling {@link AnalyticsCollector#setPlayer(Player)} before using the analytics + * collector. * @param clock A {@link Clock} used to generate timestamps. * @return An analytics collector. */ - public AnalyticsCollector createAnalyticsCollector(Player player, Clock clock) { + public AnalyticsCollector createAnalyticsCollector(@Nullable Player player, Clock clock) { return new AnalyticsCollector(player, clock); } } private final CopyOnWriteArraySet listeners; - private final Player player; private final Clock clock; private final Window window; private final MediaPeriodQueueTracker mediaPeriodQueueTracker; + private @MonotonicNonNull Player player; + /** * Creates an analytics collector for the specified player. * - * @param player The {@link Player} for which data will be collected. + * @param player The {@link Player} for which data will be collected. Can be null, if the player + * is set by calling {@link AnalyticsCollector#setPlayer(Player)} before using the analytics + * collector. * @param clock A {@link Clock} used to generate timestamps. */ - protected AnalyticsCollector(Player player, Clock clock) { - this.player = Assertions.checkNotNull(player); + protected AnalyticsCollector(@Nullable Player player, Clock clock) { + this.player = player; this.clock = Assertions.checkNotNull(clock); listeners = new CopyOnWriteArraySet<>(); mediaPeriodQueueTracker = new MediaPeriodQueueTracker(); @@ -113,6 +123,17 @@ public class AnalyticsCollector listeners.remove(listener); } + /** + * Sets the player for which data will be collected. Must only be called if no player has been set + * yet. + * + * @param player The {@link Player} for which data will be collected. + */ + public void setPlayer(Player player) { + Assertions.checkState(this.player == null); + this.player = Assertions.checkNotNull(player); + } + // External events. /** @@ -129,31 +150,6 @@ public class AnalyticsCollector } } - /** - * Notify analytics collector that the viewport size changed. - * - * @param width The new width of the viewport in device-independent pixels (dp). - * @param height The new height of the viewport in device-independent pixels (dp). - */ - public final void notifyViewportSizeChanged(int width, int height) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); - for (AnalyticsListener listener : listeners) { - listener.onViewportSizeChange(eventTime, width, height); - } - } - - /** - * Notify analytics collector that the network type or connectivity changed. - * - * @param networkInfo The new network info, or null if no network connection exists. - */ - public final void notifyNetworkTypeChanged(@Nullable NetworkInfo networkInfo) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); - for (AnalyticsListener listener : listeners) { - listener.onNetworkTypeChanged(eventTime, networkInfo); - } - } - /** * Resets the analytics collector for a new media source. Should be called before the player is * prepared with a new media source. @@ -188,14 +184,6 @@ public class AnalyticsCollector } } - @Override - public final void onAudioSessionId(int audioSessionId) { - EventTime eventTime = generateReadingMediaPeriodEventTime(); - for (AnalyticsListener listener : listeners) { - listener.onAudioSessionId(eventTime, audioSessionId); - } - } - @Override public final void onAudioDecoderInitialized( String decoderName, long initializedTimestampMs, long initializationDurationMs) { @@ -233,6 +221,32 @@ public class AnalyticsCollector } } + // AudioListener implementation. + + @Override + public final void onAudioSessionId(int audioSessionId) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onAudioSessionId(eventTime, audioSessionId); + } + } + + @Override + public void onAudioAttributesChanged(AudioAttributes audioAttributes) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onAudioAttributesChanged(eventTime, audioAttributes); + } + } + + @Override + public void onVolumeChanged(float audioVolume) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onVolumeChanged(eventTime, audioVolume); + } + } + // VideoRendererEventListener implementation. @Override @@ -271,12 +285,12 @@ public class AnalyticsCollector } @Override - public final void onVideoSizeChanged( - int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - EventTime eventTime = generateReadingMediaPeriodEventTime(); + public final void onVideoDisabled(DecoderCounters counters) { + // The renderers are disabled after we changed the playing media period on the playback thread + // but before this change is reported to the app thread. + EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { - listener.onVideoSizeChanged( - eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio); + listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_VIDEO, counters); } } @@ -288,16 +302,31 @@ public class AnalyticsCollector } } + // VideoListener implementation. + @Override - public final void onVideoDisabled(DecoderCounters counters) { - // The renderers are disabled after we changed the playing media period on the playback thread - // but before this change is reported to the app thread. - EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime(); + public final void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { - listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_VIDEO, counters); + listener.onVideoSizeChanged( + eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } } + @Override + public void onSurfaceSizeChanged(int width, int height) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onSurfaceSizeChanged(eventTime, width, height); + } + } + + @Override + public final void onRenderedFirstFrame() { + // Do nothing. Already reported in VideoRendererEventListener.onRenderedFirstFrame. + } + // MediaSourceEventListener implementation. @Override @@ -403,7 +432,7 @@ public class AnalyticsCollector @Override public final void onTimelineChanged( - Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { + Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { mediaPeriodQueueTracker.onTimelineChanged(timeline); EventTime eventTime = generatePlayingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { @@ -541,6 +570,7 @@ public class AnalyticsCollector /** Returns a new {@link EventTime} for the specified window index and media period id. */ protected EventTime generateEventTime(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { + Assertions.checkNotNull(player); long realtimeMs = clock.elapsedRealtime(); Timeline timeline = player.getCurrentTimeline(); long eventPositionMs; @@ -565,8 +595,6 @@ public class AnalyticsCollector // This event is for content in a future window. Assume default start position. eventPositionMs = timeline.getWindow(windowIndex, window).getDefaultPositionMs(); } - // TODO(b/30792113): implement this properly (player.getTotalBufferedDuration()). - long bufferedDurationMs = player.getBufferedPosition() - player.getContentPosition(); return new EventTime( realtimeMs, timeline, @@ -574,12 +602,12 @@ public class AnalyticsCollector mediaPeriodId, eventPositionMs, player.getCurrentPosition(), - bufferedDurationMs); + player.getTotalBufferedDuration()); } private EventTime generateEventTime(@Nullable WindowAndMediaPeriodId mediaPeriod) { if (mediaPeriod == null) { - int windowIndex = player.getCurrentWindowIndex(); + int windowIndex = Assertions.checkNotNull(player).getCurrentWindowIndex(); MediaPeriodId mediaPeriodId = mediaPeriodQueueTracker.tryResolveWindowIndex(windowIndex); return generateEventTime(windowIndex, mediaPeriodId); } @@ -756,8 +784,7 @@ public class AnalyticsCollector if (newTimeline.isEmpty() || timeline.isEmpty()) { return mediaPeriod; } - Object uid = - timeline.getPeriod(mediaPeriod.mediaPeriodId.periodIndex, period, /* setIds= */ true).uid; + Object uid = timeline.getUidOfPeriod(mediaPeriod.mediaPeriodId.periodIndex); int newPeriodIndex = newTimeline.getIndexOfPeriod(uid); if (newPeriodIndex == C.INDEX_UNSET) { return mediaPeriod; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java index 48057f2bff..adc4b3cdb9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.analytics; -import android.net.NetworkInfo; import android.support.annotation.Nullable; import android.view.Surface; import com.google.android.exoplayer2.C; @@ -26,6 +25,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.TimelineChangeReason; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.metadata.Metadata; @@ -41,6 +41,8 @@ import java.io.IOException; * *

    All events are recorded with an {@link EventTime} specifying the elapsed real time and media * time at the time of the event. + * + *

    All methods have no-op default implementations to allow selective overrides. */ public interface AnalyticsListener { @@ -127,7 +129,8 @@ public interface AnalyticsListener { * @param playWhenReady Whether the playback will proceed when ready. * @param playbackState One of the {@link Player}.STATE constants. */ - void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int playbackState); + default void onPlayerStateChanged( + EventTime eventTime, boolean playWhenReady, int playbackState) {} /** * Called when the timeline changed. @@ -135,7 +138,7 @@ public interface AnalyticsListener { * @param eventTime The event time. * @param reason The reason for the timeline change. */ - void onTimelineChanged(EventTime eventTime, @TimelineChangeReason int reason); + default void onTimelineChanged(EventTime eventTime, @TimelineChangeReason int reason) {} /** * Called when a position discontinuity occurred. @@ -143,21 +146,21 @@ public interface AnalyticsListener { * @param eventTime The event time. * @param reason The reason for the position discontinuity. */ - void onPositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason); + default void onPositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason) {} /** * Called when a seek operation started. * * @param eventTime The event time. */ - void onSeekStarted(EventTime eventTime); + default void onSeekStarted(EventTime eventTime) {} /** * Called when a seek operation was processed. * * @param eventTime The event time. */ - void onSeekProcessed(EventTime eventTime); + default void onSeekProcessed(EventTime eventTime) {} /** * Called when the playback parameters changed. @@ -165,7 +168,8 @@ public interface AnalyticsListener { * @param eventTime The event time. * @param playbackParameters The new playback parameters. */ - void onPlaybackParametersChanged(EventTime eventTime, PlaybackParameters playbackParameters); + default void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) {} /** * Called when the repeat mode changed. @@ -173,7 +177,7 @@ public interface AnalyticsListener { * @param eventTime The event time. * @param repeatMode The new repeat mode. */ - void onRepeatModeChanged(EventTime eventTime, @Player.RepeatMode int repeatMode); + default void onRepeatModeChanged(EventTime eventTime, @Player.RepeatMode int repeatMode) {} /** * Called when the shuffle mode changed. @@ -181,7 +185,7 @@ public interface AnalyticsListener { * @param eventTime The event time. * @param shuffleModeEnabled Whether the shuffle mode is enabled. */ - void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled); + default void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {} /** * Called when the player starts or stops loading data from a source. @@ -189,7 +193,7 @@ public interface AnalyticsListener { * @param eventTime The event time. * @param isLoading Whether the player is loading. */ - void onLoadingChanged(EventTime eventTime, boolean isLoading); + default void onLoadingChanged(EventTime eventTime, boolean isLoading) {} /** * Called when a fatal player error occurred. @@ -197,7 +201,7 @@ public interface AnalyticsListener { * @param eventTime The event time. * @param error The error. */ - void onPlayerError(EventTime eventTime, ExoPlaybackException error); + default void onPlayerError(EventTime eventTime, ExoPlaybackException error) {} /** * Called when the available or selected tracks for the renderers changed. @@ -206,8 +210,8 @@ public interface AnalyticsListener { * @param trackGroups The available tracks. May be empty. * @param trackSelections The track selections for each renderer. May contain null elements. */ - void onTracksChanged( - EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections); + default void onTracksChanged( + EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {} /** * Called when a media source started loading data. @@ -216,7 +220,8 @@ public interface AnalyticsListener { * @param loadEventInfo The {@link LoadEventInfo} defining the load event. * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. */ - void onLoadStarted(EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData); + default void onLoadStarted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {} /** * Called when a media source completed loading data. @@ -225,8 +230,8 @@ public interface AnalyticsListener { * @param loadEventInfo The {@link LoadEventInfo} defining the load event. * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. */ - void onLoadCompleted( - EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData); + default void onLoadCompleted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {} /** * Called when a media source canceled loading data. @@ -235,8 +240,8 @@ public interface AnalyticsListener { * @param loadEventInfo The {@link LoadEventInfo} defining the load event. * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. */ - void onLoadCanceled( - EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData); + default void onLoadCanceled( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {} /** * Called when a media source loading error occurred. These errors are just for informational @@ -248,12 +253,12 @@ public interface AnalyticsListener { * @param error The load error. * @param wasCanceled Whether the load was canceled as a result of the error. */ - void onLoadError( + default void onLoadError( EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, - boolean wasCanceled); + boolean wasCanceled) {} /** * Called when the downstream format sent to the renderers changed. @@ -261,7 +266,7 @@ public interface AnalyticsListener { * @param eventTime The event time. * @param mediaLoadData The {@link MediaLoadData} defining the newly selected media data. */ - void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData); + default void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {} /** * Called when data is removed from the back of a media buffer, typically so that it can be @@ -270,28 +275,28 @@ public interface AnalyticsListener { * @param eventTime The event time. * @param mediaLoadData The {@link MediaLoadData} defining the media being discarded. */ - void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData); + default void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) {} /** * Called when a media source created a media period. * * @param eventTime The event time. */ - void onMediaPeriodCreated(EventTime eventTime); + default void onMediaPeriodCreated(EventTime eventTime) {} /** * Called when a media source released a media period. * * @param eventTime The event time. */ - void onMediaPeriodReleased(EventTime eventTime); + default void onMediaPeriodReleased(EventTime eventTime) {} /** * Called when the player started reading a media period. * * @param eventTime The event time. */ - void onReadingStarted(EventTime eventTime); + default void onReadingStarted(EventTime eventTime) {} /** * Called when the bandwidth estimate for the current data source has been updated. @@ -301,25 +306,19 @@ public interface AnalyticsListener { * @param totalBytesLoaded The total bytes loaded this update is based on. * @param bitrateEstimate The bandwidth estimate, in bits per second. */ - void onBandwidthEstimate( - EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate); + default void onBandwidthEstimate( + EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {} /** - * Called when the viewport size of the output surface changed. + * Called when the output surface size changed. * * @param eventTime The event time. - * @param width The width of the viewport in device-independent pixels (dp). - * @param height The height of the viewport in device-independent pixels (dp). + * @param width The surface width in pixels. May be {@link C#LENGTH_UNSET} if unknown, or 0 if the + * video is not rendered onto a surface. + * @param height The surface height in pixels. May be {@link C#LENGTH_UNSET} if unknown, or 0 if + * the video is not rendered onto a surface. */ - void onViewportSizeChange(EventTime eventTime, int width, int height); - - /** - * Called when the type of the network connection changed. - * - * @param eventTime The event time. - * @param networkInfo The network info for the current connection, or null if disconnected. - */ - void onNetworkTypeChanged(EventTime eventTime, @Nullable NetworkInfo networkInfo); + default void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {} /** * Called when there is {@link Metadata} associated with the current playback time. @@ -327,7 +326,7 @@ public interface AnalyticsListener { * @param eventTime The event time. * @param metadata The metadata. */ - void onMetadata(EventTime eventTime, Metadata metadata); + default void onMetadata(EventTime eventTime, Metadata metadata) {} /** * Called when an audio or video decoder has been enabled. @@ -337,7 +336,8 @@ public interface AnalyticsListener { * {@link C#TRACK_TYPE_VIDEO}. * @param decoderCounters The accumulated event counters associated with this decoder. */ - void onDecoderEnabled(EventTime eventTime, int trackType, DecoderCounters decoderCounters); + default void onDecoderEnabled( + EventTime eventTime, int trackType, DecoderCounters decoderCounters) {} /** * Called when an audio or video decoder has been initialized. @@ -348,8 +348,8 @@ public interface AnalyticsListener { * @param decoderName The decoder that was created. * @param initializationDurationMs Time taken to initialize the decoder, in milliseconds. */ - void onDecoderInitialized( - EventTime eventTime, int trackType, String decoderName, long initializationDurationMs); + default void onDecoderInitialized( + EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) {} /** * Called when an audio or video decoder input format changed. @@ -359,7 +359,7 @@ public interface AnalyticsListener { * C#TRACK_TYPE_AUDIO} or {@link C#TRACK_TYPE_VIDEO}. * @param format The new input format for the decoder. */ - void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format); + default void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {} /** * Called when an audio or video decoder has been disabled. @@ -369,7 +369,8 @@ public interface AnalyticsListener { * {@link C#TRACK_TYPE_VIDEO}. * @param decoderCounters The accumulated event counters associated with this decoder. */ - void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters decoderCounters); + default void onDecoderDisabled( + EventTime eventTime, int trackType, DecoderCounters decoderCounters) {} /** * Called when the audio session id is set. @@ -377,7 +378,23 @@ public interface AnalyticsListener { * @param eventTime The event time. * @param audioSessionId The audio session id. */ - void onAudioSessionId(EventTime eventTime, int audioSessionId); + default void onAudioSessionId(EventTime eventTime, int audioSessionId) {} + + /** + * Called when the audio attributes change. + * + * @param eventTime The event time. + * @param audioAttributes The audio attributes. + */ + default void onAudioAttributesChanged(EventTime eventTime, AudioAttributes audioAttributes) {} + + /** + * Called when the volume changes. + * + * @param eventTime The event time. + * @param volume The new volume, with 0 being silence and 1 being unity gain. + */ + default void onVolumeChanged(EventTime eventTime, float volume) {} /** * Called when an audio underrun occurred. @@ -389,8 +406,8 @@ public interface AnalyticsListener { * as the buffered media can have a variable bitrate so the duration may be unknown. * @param elapsedSinceLastFeedMs The time since the {@link AudioSink} was last fed data. */ - void onAudioUnderrun( - EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs); + default void onAudioUnderrun( + EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {} /** * Called after video frames have been dropped. @@ -401,7 +418,7 @@ public interface AnalyticsListener { * is timed from when the renderer was started or from when dropped frames were last reported * (whichever was more recent), and not from when the first of the reported drops occurred. */ - void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs); + default void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {} /** * Called before a frame is rendered for the first time since setting the surface, and each time @@ -416,12 +433,12 @@ public interface AnalyticsListener { * since the renderer will apply all necessary rotations internally. * @param pixelWidthHeightRatio The width to height ratio of each pixel. */ - void onVideoSizeChanged( + default void onVideoSizeChanged( EventTime eventTime, int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio); + float pixelWidthHeightRatio) {} /** * Called when a frame is rendered for the first time since setting the surface, and when a frame @@ -431,14 +448,14 @@ public interface AnalyticsListener { * @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if * the renderer renders to something that isn't a {@link Surface}. */ - void onRenderedFirstFrame(EventTime eventTime, Surface surface); + default void onRenderedFirstFrame(EventTime eventTime, Surface surface) {} /** * Called each time drm keys are loaded. * * @param eventTime The event time. */ - void onDrmKeysLoaded(EventTime eventTime); + default void onDrmKeysLoaded(EventTime eventTime) {} /** * Called when a drm error occurs. These errors are just for informational purposes and the player @@ -447,19 +464,19 @@ public interface AnalyticsListener { * @param eventTime The event time. * @param error The error. */ - void onDrmSessionManagerError(EventTime eventTime, Exception error); + default void onDrmSessionManagerError(EventTime eventTime, Exception error) {} /** * Called each time offline drm keys are restored. * * @param eventTime The event time. */ - void onDrmKeysRestored(EventTime eventTime); + default void onDrmKeysRestored(EventTime eventTime) {} /** * Called each time offline drm keys are removed. * * @param eventTime The event time. */ - void onDrmKeysRemoved(EventTime eventTime); + default void onDrmKeysRemoved(EventTime eventTime) {} } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java index 4a49de56b0..d487a8aa99 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java @@ -15,152 +15,9 @@ */ package com.google.android.exoplayer2.analytics; -import android.net.NetworkInfo; -import android.view.Surface; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.decoder.DecoderCounters; -import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; -import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import java.io.IOException; - /** - * {@link AnalyticsListener} allowing selective overrides. All methods are implemented as no-ops. + * @deprecated Use {@link AnalyticsListener} directly for selective overrides as all methods are + * implemented as no-op default methods. */ -public abstract class DefaultAnalyticsListener implements AnalyticsListener { - - @Override - public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int playbackState) {} - - @Override - public void onTimelineChanged(EventTime eventTime, int reason) {} - - @Override - public void onPositionDiscontinuity(EventTime eventTime, int reason) {} - - @Override - public void onSeekStarted(EventTime eventTime) {} - - @Override - public void onSeekProcessed(EventTime eventTime) {} - - @Override - public void onPlaybackParametersChanged( - EventTime eventTime, PlaybackParameters playbackParameters) {} - - @Override - public void onRepeatModeChanged(EventTime eventTime, int repeatMode) {} - - @Override - public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {} - - @Override - public void onLoadingChanged(EventTime eventTime, boolean isLoading) {} - - @Override - public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {} - - @Override - public void onTracksChanged( - EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {} - - @Override - public void onLoadStarted( - EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {} - - @Override - public void onLoadCompleted( - EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {} - - @Override - public void onLoadCanceled( - EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {} - - @Override - public void onLoadError( - EventTime eventTime, - LoadEventInfo loadEventInfo, - MediaLoadData mediaLoadData, - IOException error, - boolean wasCanceled) {} - - @Override - public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {} - - @Override - public void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) {} - - @Override - public void onMediaPeriodCreated(EventTime eventTime) {} - - @Override - public void onMediaPeriodReleased(EventTime eventTime) {} - - @Override - public void onReadingStarted(EventTime eventTime) {} - - @Override - public void onBandwidthEstimate( - EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {} - - @Override - public void onViewportSizeChange(EventTime eventTime, int width, int height) {} - - @Override - public void onNetworkTypeChanged(EventTime eventTime, NetworkInfo networkInfo) {} - - @Override - public void onMetadata(EventTime eventTime, Metadata metadata) {} - - @Override - public void onDecoderEnabled( - EventTime eventTime, int trackType, DecoderCounters decoderCounters) {} - - @Override - public void onDecoderInitialized( - EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) {} - - @Override - public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {} - - @Override - public void onDecoderDisabled( - EventTime eventTime, int trackType, DecoderCounters decoderCounters) {} - - @Override - public void onAudioSessionId(EventTime eventTime, int audioSessionId) {} - - @Override - public void onAudioUnderrun( - EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {} - - @Override - public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {} - - @Override - public void onVideoSizeChanged( - EventTime eventTime, - int width, - int height, - int unappliedRotationDegrees, - float pixelWidthHeightRatio) {} - - @Override - public void onRenderedFirstFrame(EventTime eventTime, Surface surface) {} - - @Override - public void onDrmKeysLoaded(EventTime eventTime) {} - - @Override - public void onDrmSessionManagerError(EventTime eventTime, Exception error) {} - - @Override - public void onDrmKeysRestored(EventTime eventTime) {} - - @Override - public void onDrmKeysRemoved(EventTime eventTime) {} -} +@Deprecated +public abstract class DefaultAnalyticsListener implements AnalyticsListener {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index c61b8ff24c..94fe759a9b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -137,17 +137,17 @@ public final class Ac3Util { 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393}; /** - * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to - * ETSI TS 102 366 Annex F. The reading position of {@code data} will be modified. + * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to ETSI TS + * 102 366 Annex F. The reading position of {@code data} will be modified. * * @param data The AC3SpecificBox to parse. - * @param trackId The track identifier to set on the format, or null. + * @param trackId The track identifier to set on the format. * @param language The language to set on the format. * @param drmInitData {@link DrmInitData} to be included in the format. * @return The AC-3 format parsed from data in the header. */ - public static Format parseAc3AnnexFFormat(ParsableByteArray data, String trackId, - String language, DrmInitData drmInitData) { + public static Format parseAc3AnnexFFormat( + ParsableByteArray data, String trackId, String language, DrmInitData drmInitData) { int fscod = (data.readUnsignedByte() & 0xC0) >> 6; int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; int nextByte = data.readUnsignedByte(); @@ -155,22 +155,32 @@ public final class Ac3Util { if ((nextByte & 0x04) != 0) { // lfeon channelCount++; } - return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_AC3, null, Format.NO_VALUE, - Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); + return Format.createAudioSampleFormat( + trackId, + MimeTypes.AUDIO_AC3, + /* codecs= */ null, + Format.NO_VALUE, + Format.NO_VALUE, + channelCount, + sampleRate, + /* initializationData= */ null, + drmInitData, + /* selectionFlags= */ 0, + language); } /** - * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to - * ETSI TS 102 366 Annex F. The reading position of {@code data} will be modified. + * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to ETSI TS + * 102 366 Annex F. The reading position of {@code data} will be modified. * * @param data The EC3SpecificBox to parse. - * @param trackId The track identifier to set on the format, or null. + * @param trackId The track identifier to set on the format. * @param language The language to set on the format. * @param drmInitData {@link DrmInitData} to be included in the format. * @return The E-AC-3 format parsed from data in the header. */ - public static Format parseEAc3AnnexFFormat(ParsableByteArray data, String trackId, - String language, DrmInitData drmInitData) { + public static Format parseEAc3AnnexFFormat( + ParsableByteArray data, String trackId, String language, DrmInitData drmInitData) { data.skipBytes(2); // data_rate, num_ind_sub // Read the first independent substream. @@ -200,8 +210,18 @@ public final class Ac3Util { mimeType = MimeTypes.AUDIO_E_AC3_JOC; } } - return Format.createAudioSampleFormat(trackId, mimeType, null, Format.NO_VALUE, - Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); + return Format.createAudioSampleFormat( + trackId, + mimeType, + /* codecs= */ null, + Format.NO_VALUE, + Format.NO_VALUE, + channelCount, + sampleRate, + /* initializationData= */ null, + drmInitData, + /* selectionFlags= */ 0, + language); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java index 5e963a2540..848b3ee10c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java @@ -39,12 +39,9 @@ public final class AudioAttributes { */ public static final class Builder { - @C.AudioContentType - private int contentType; - @C.AudioFlags - private int flags; - @C.AudioUsage - private int usage; + private @C.AudioContentType int contentType; + private @C.AudioFlags int flags; + private @C.AudioUsage int usage; /** * Creates a new builder for {@link AudioAttributes}. @@ -91,14 +88,11 @@ public final class AudioAttributes { } - @C.AudioContentType - public final int contentType; - @C.AudioFlags - public final int flags; - @C.AudioUsage - public final int usage; + public final @C.AudioContentType int contentType; + public final @C.AudioFlags int flags; + public final @C.AudioUsage int usage; - private android.media.AudioAttributes audioAttributesV21; + private @Nullable android.media.AudioAttributes audioAttributesV21; private AudioAttributes(@C.AudioContentType int contentType, @C.AudioFlags int flags, @C.AudioUsage int usage) { @@ -108,7 +102,7 @@ public final class AudioAttributes { } @TargetApi(21) - /* package */ android.media.AudioAttributes getAudioAttributesV21() { + public android.media.AudioAttributes getAudioAttributesV21() { if (audioAttributesV21 == null) { audioAttributesV21 = new android.media.AudioAttributes.Builder() .setContentType(contentType) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java index 4b03a5047b..92d39dec65 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java @@ -25,9 +25,7 @@ import android.media.AudioManager; import android.support.annotation.Nullable; import java.util.Arrays; -/** - * Represents the set of audio formats that a device is capable of playing. - */ +/** Represents the set of audio formats that a device is capable of playing. */ @TargetApi(21) public final class AudioCapabilities { @@ -50,7 +48,7 @@ public final class AudioCapabilities { } @SuppressLint("InlinedApi") - /* package */ static AudioCapabilities getCapabilities(Intent intent) { + /* package */ static AudioCapabilities getCapabilities(@Nullable Intent intent) { if (intent == null || intent.getIntExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 0) == 0) { return DEFAULT_AUDIO_CAPABILITIES; } @@ -65,11 +63,15 @@ public final class AudioCapabilities { * Constructs new audio capabilities based on a set of supported encodings and a maximum channel * count. * + *

    Applications should generally call {@link #getCapabilities(Context)} to obtain an instance + * based on the capabilities advertised by the platform, rather than calling this constructor. + * * @param supportedEncodings Supported audio encodings from {@link android.media.AudioFormat}'s - * {@code ENCODING_*} constants. + * {@code ENCODING_*} constants. Passing {@code null} indicates that no encodings are + * supported. * @param maxChannelCount The maximum number of audio channels that can be played simultaneously. */ - /* package */ AudioCapabilities(int[] supportedEncodings, int maxChannelCount) { + public AudioCapabilities(@Nullable int[] supportedEncodings, int maxChannelCount) { if (supportedEncodings != null) { this.supportedEncodings = Arrays.copyOf(supportedEncodings, supportedEncodings.length); Arrays.sort(this.supportedEncodings); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java index 49ec96e3d6..aa610db8b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java @@ -20,6 +20,8 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; +import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -44,17 +46,29 @@ public final class AudioCapabilitiesReceiver { } private final Context context; + private final @Nullable Handler handler; private final Listener listener; - private final BroadcastReceiver receiver; + private final @Nullable BroadcastReceiver receiver; - /* package */ AudioCapabilities audioCapabilities; + /* package */ @Nullable AudioCapabilities audioCapabilities; /** * @param context A context for registering the receiver. * @param listener The listener to notify when audio capabilities change. */ public AudioCapabilitiesReceiver(Context context, Listener listener) { + this(context, /* handler= */ null, listener); + } + + /** + * @param context A context for registering the receiver. + * @param handler The handler to which {@link Listener} events will be posted. If null, listener + * methods are invoked on the main thread. + * @param listener The listener to notify when audio capabilities change. + */ + public AudioCapabilitiesReceiver(Context context, @Nullable Handler handler, Listener listener) { this.context = Assertions.checkNotNull(context); + this.handler = handler; this.listener = Assertions.checkNotNull(listener); this.receiver = Util.SDK_INT >= 21 ? new HdmiAudioPlugBroadcastReceiver() : null; } @@ -68,8 +82,17 @@ public final class AudioCapabilitiesReceiver { */ @SuppressWarnings("InlinedApi") public AudioCapabilities register() { - Intent stickyIntent = receiver == null ? null - : context.registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG)); + Intent stickyIntent = null; + if (receiver != null) { + IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG); + if (handler != null) { + stickyIntent = + context.registerReceiver( + receiver, intentFilter, /* broadcastPermission= */ null, handler); + } else { + stickyIntent = context.registerReceiver(receiver, intentFilter); + } + } audioCapabilities = AudioCapabilities.getCapabilities(stickyIntent); return audioCapabilities; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java new file mode 100644 index 0000000000..d078cddcc1 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java @@ -0,0 +1,454 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.audio; + +import android.content.Context; +import android.media.AudioFocusRequest; +import android.media.AudioManager; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.util.Log; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** Manages requesting and responding to changes in audio focus. */ +public final class AudioFocusManager { + + /** Interface to allow AudioFocusManager to give commands to a player. */ + public interface PlayerControl { + /** + * Called when the volume multiplier on the player should be changed. + * + * @param volumeMultiplier The new volume multiplier. + */ + void setVolumeMultiplier(float volumeMultiplier); + + /** + * Called when a command must be executed on the player. + * + * @param playerCommand The command that must be executed. + */ + void executePlayerCommand(@PlayerCommand int playerCommand); + } + + /** Player commands. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + PLAYER_COMMAND_DO_NOT_PLAY, + PLAYER_COMMAND_WAIT_FOR_CALLBACK, + PLAYER_COMMAND_PLAY_WHEN_READY, + }) + public @interface PlayerCommand {} + /** Do not play. */ + public static final int PLAYER_COMMAND_DO_NOT_PLAY = -1; + /** Do not play now. Wait for callback to play. */ + public static final int PLAYER_COMMAND_WAIT_FOR_CALLBACK = 0; + /** Play freely. */ + public static final int PLAYER_COMMAND_PLAY_WHEN_READY = 1; + + /** Audio focus state. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + AUDIO_FOCUS_STATE_LOST_FOCUS, + AUDIO_FOCUS_STATE_NO_FOCUS, + AUDIO_FOCUS_STATE_HAVE_FOCUS, + AUDIO_FOCUS_STATE_LOSS_TRANSIENT, + AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK + }) + private @interface AudioFocusState {} + /** No audio focus was held, but has been lost by another app taking it permanently. */ + private static final int AUDIO_FOCUS_STATE_LOST_FOCUS = -1; + /** No audio focus is currently being held. */ + private static final int AUDIO_FOCUS_STATE_NO_FOCUS = 0; + /** The requested audio focus is currently held. */ + private static final int AUDIO_FOCUS_STATE_HAVE_FOCUS = 1; + /** Audio focus has been temporarily lost. */ + private static final int AUDIO_FOCUS_STATE_LOSS_TRANSIENT = 2; + /** Audio focus has been temporarily lost, but playback may continue with reduced volume. */ + private static final int AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK = 3; + + private static final String TAG = "AudioFocusManager"; + + private static final float VOLUME_MULTIPLIER_DUCK = 0.2f; + private static final float VOLUME_MULTIPLIER_DEFAULT = 1.0f; + + private final @Nullable AudioManager audioManager; + private final AudioFocusListener focusListener; + private final PlayerControl playerControl; + private @Nullable AudioAttributes audioAttributes; + + private @AudioFocusState int audioFocusState; + private int focusGain; + private float volumeMultiplier = 1.0f; + + private @MonotonicNonNull AudioFocusRequest audioFocusRequest; + private boolean rebuildAudioFocusRequest; + + /** + * Constructs an AudioFocusManager to automatically handle audio focus for a player. + * + * @param context The current context. + * @param playerControl A {@link PlayerControl} to handle commands from this instance. + */ + public AudioFocusManager(@Nullable Context context, PlayerControl playerControl) { + this.audioManager = + context == null + ? null + : (AudioManager) + context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE); + this.playerControl = playerControl; + this.focusListener = new AudioFocusListener(); + this.audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS; + } + + /** Gets the current player volume multiplier. */ + public float getVolumeMultiplier() { + return volumeMultiplier; + } + + /** + * Sets audio attributes that should be used to manage audio focus. + * + * @param audioAttributes The audio attributes or {@code null} if audio focus should not be + * managed automatically. + * @param playWhenReady The current state of {@link ExoPlayer#getPlayWhenReady()}. + * @param playerState The current player state; {@link ExoPlayer#getPlaybackState()}. + * @return A command to execute on the player. One of {@link #PLAYER_COMMAND_DO_NOT_PLAY}, {@link + * #PLAYER_COMMAND_WAIT_FOR_CALLBACK}, and {@link #PLAYER_COMMAND_PLAY_WHEN_READY}. + */ + public @PlayerCommand int setAudioAttributes( + @Nullable AudioAttributes audioAttributes, boolean playWhenReady, int playerState) { + if (audioAttributes == null) { + return PLAYER_COMMAND_PLAY_WHEN_READY; + } + + Assertions.checkNotNull( + audioManager, "SimpleExoPlayer must be created with a context to handle audio focus."); + if (!Util.areEqual(this.audioAttributes, audioAttributes)) { + this.audioAttributes = audioAttributes; + focusGain = convertAudioAttributesToFocusGain(audioAttributes); + + Assertions.checkArgument( + focusGain == C.AUDIOFOCUS_GAIN || focusGain == C.AUDIOFOCUS_NONE, + "Automatic handling of audio focus is only available for USAGE_MEDIA and USAGE_GAME."); + if (playWhenReady + && (playerState == Player.STATE_BUFFERING || playerState == Player.STATE_READY)) { + return requestAudioFocus(); + } + } + + if (playerState == Player.STATE_IDLE) { + return PLAYER_COMMAND_WAIT_FOR_CALLBACK; + } else { + return handlePrepare(playWhenReady); + } + } + + /** + * Called by a player as part of {@link ExoPlayer#prepare(MediaSource, boolean, boolean)}. + * + * @param playWhenReady The current state of {@link ExoPlayer#getPlayWhenReady()}. + * @return A command to execute on the player. One of {@link #PLAYER_COMMAND_DO_NOT_PLAY}, {@link + * #PLAYER_COMMAND_WAIT_FOR_CALLBACK}, and {@link #PLAYER_COMMAND_PLAY_WHEN_READY}. + */ + public @PlayerCommand int handlePrepare(boolean playWhenReady) { + if (audioManager == null) { + return PLAYER_COMMAND_PLAY_WHEN_READY; + } + + return playWhenReady ? requestAudioFocus() : PLAYER_COMMAND_DO_NOT_PLAY; + } + + /** + * Called by the player as part of {@link ExoPlayer#setPlayWhenReady(boolean)}. + * + * @param playWhenReady The desired value of playWhenReady. + * @param playerState The current state of the player. + * @return A command to execute on the player. One of {@link #PLAYER_COMMAND_DO_NOT_PLAY}, {@link + * #PLAYER_COMMAND_WAIT_FOR_CALLBACK}, and {@link #PLAYER_COMMAND_PLAY_WHEN_READY}. + */ + public @PlayerCommand int handleSetPlayWhenReady(boolean playWhenReady, int playerState) { + if (audioManager == null) { + return PLAYER_COMMAND_PLAY_WHEN_READY; + } + + if (!playWhenReady) { + abandonAudioFocus(); + return PLAYER_COMMAND_DO_NOT_PLAY; + } else if (playerState != Player.STATE_IDLE) { + return requestAudioFocus(); + } + return focusGain != C.AUDIOFOCUS_NONE + ? PLAYER_COMMAND_WAIT_FOR_CALLBACK + : PLAYER_COMMAND_PLAY_WHEN_READY; + } + + /** Called by the player as part of {@link ExoPlayer#stop(boolean)}. */ + public void handleStop() { + if (audioManager == null) { + return; + } + + abandonAudioFocus(/* forceAbandon= */ true); + } + + // Internal methods. + + private @PlayerCommand int requestAudioFocus() { + int focusRequestResult; + + if (focusGain == C.AUDIOFOCUS_NONE) { + if (audioFocusState != AUDIO_FOCUS_STATE_NO_FOCUS) { + abandonAudioFocus(/* forceAbandon= */ true); + } + return PLAYER_COMMAND_PLAY_WHEN_READY; + } + + if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { + if (Util.SDK_INT >= 26) { + focusRequestResult = requestAudioFocusV26(); + } else { + focusRequestResult = requestAudioFocusDefault(); + } + audioFocusState = + focusRequestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED + ? AUDIO_FOCUS_STATE_HAVE_FOCUS + : AUDIO_FOCUS_STATE_NO_FOCUS; + } + + if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { + return PLAYER_COMMAND_DO_NOT_PLAY; + } + + return audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT + ? PLAYER_COMMAND_WAIT_FOR_CALLBACK + : PLAYER_COMMAND_PLAY_WHEN_READY; + } + + private void abandonAudioFocus() { + abandonAudioFocus(/* forceAbandon= */ false); + } + + private void abandonAudioFocus(boolean forceAbandon) { + if (focusGain == C.AUDIOFOCUS_NONE && audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { + return; + } + + if (focusGain != C.AUDIOFOCUS_GAIN + || audioFocusState == AUDIO_FOCUS_STATE_LOST_FOCUS + || forceAbandon) { + if (Util.SDK_INT >= 26) { + abandonAudioFocusV26(); + } else { + abandonAudioFocusDefault(); + } + audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS; + } + } + + private int requestAudioFocusDefault() { + AudioManager audioManager = Assertions.checkNotNull(this.audioManager); + return audioManager.requestAudioFocus( + focusListener, + Util.getStreamTypeForAudioUsage(Assertions.checkNotNull(audioAttributes).usage), + focusGain); + } + + @RequiresApi(26) + private int requestAudioFocusV26() { + if (audioFocusRequest == null || rebuildAudioFocusRequest) { + AudioFocusRequest.Builder builder = + audioFocusRequest == null + ? new AudioFocusRequest.Builder(focusGain) + : new AudioFocusRequest.Builder(audioFocusRequest); + + boolean willPauseWhenDucked = willPauseWhenDucked(); + audioFocusRequest = + builder + .setAudioAttributes(Assertions.checkNotNull(audioAttributes).getAudioAttributesV21()) + .setWillPauseWhenDucked(willPauseWhenDucked) + .setOnAudioFocusChangeListener(focusListener) + .build(); + + rebuildAudioFocusRequest = false; + } + return Assertions.checkNotNull(audioManager).requestAudioFocus(audioFocusRequest); + } + + private void abandonAudioFocusDefault() { + Assertions.checkNotNull(audioManager).abandonAudioFocus(focusListener); + } + + @RequiresApi(26) + private void abandonAudioFocusV26() { + if (audioFocusRequest != null) { + Assertions.checkNotNull(audioManager).abandonAudioFocusRequest(audioFocusRequest); + } + } + + private boolean willPauseWhenDucked() { + return audioAttributes != null && audioAttributes.contentType == C.CONTENT_TYPE_SPEECH; + } + + /** + * Converts {@link AudioAttributes} to one of the audio focus request. + * + *

    This follows the class Javadoc of {@link AudioFocusRequest}. + * + * @param audioAttributes The audio attributes associated with this focus request. + * @return The type of audio focus gain that should be requested. + */ + private static int convertAudioAttributesToFocusGain(@Nullable AudioAttributes audioAttributes) { + + if (audioAttributes == null) { + // Don't handle audio focus. It may be either video only contents or developers + // want to have more finer grained control. (e.g. adding audio focus listener) + return C.AUDIOFOCUS_NONE; + } + + switch (audioAttributes.usage) { + // USAGE_VOICE_COMMUNICATION_SIGNALLING is for DTMF that may happen multiple times + // during the phone call when AUDIOFOCUS_GAIN_TRANSIENT is requested for that. + // Don't request audio focus here. + case C.USAGE_VOICE_COMMUNICATION_SIGNALLING: + return C.AUDIOFOCUS_NONE; + + // Javadoc says 'AUDIOFOCUS_GAIN: Examples of uses of this focus gain are for music + // playback, for a game or a video player' + case C.USAGE_GAME: + case C.USAGE_MEDIA: + return C.AUDIOFOCUS_GAIN; + + // Special usages: USAGE_UNKNOWN shouldn't be used. Request audio focus to prevent + // multiple media playback happen at the same time. + case C.USAGE_UNKNOWN: + Log.w( + TAG, + "Specify a proper usage in the audio attributes for audio focus" + + " handling. Using AUDIOFOCUS_GAIN by default."); + return C.AUDIOFOCUS_GAIN; + + // Javadoc says 'AUDIOFOCUS_GAIN_TRANSIENT: An example is for playing an alarm, or + // during a VoIP call' + case C.USAGE_ALARM: + case C.USAGE_VOICE_COMMUNICATION: + return C.AUDIOFOCUS_GAIN_TRANSIENT; + + // Javadoc says 'AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: Examples are when playing + // driving directions or notifications' + case C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: + case C.USAGE_ASSISTANCE_SONIFICATION: + case C.USAGE_NOTIFICATION: + case C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED: + case C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT: + case C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST: + case C.USAGE_NOTIFICATION_EVENT: + case C.USAGE_NOTIFICATION_RINGTONE: + return C.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK; + + // Javadoc says 'AUDIOFOCUS_GAIN_EXCLUSIVE: This is typically used if you are doing + // audio recording or speech recognition'. + // Assistant is considered as both recording and notifying developer + case C.USAGE_ASSISTANT: + if (Util.SDK_INT >= 19) { + return C.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE; + } else { + return C.AUDIOFOCUS_GAIN_TRANSIENT; + } + + // Special usages: + case C.USAGE_ASSISTANCE_ACCESSIBILITY: + if (audioAttributes.contentType == C.CONTENT_TYPE_SPEECH) { + // Voice shouldn't be interrupted by other playback. + return C.AUDIOFOCUS_GAIN_TRANSIENT; + } + return C.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK; + default: + Log.w(TAG, "Unidentified audio usage: " + audioAttributes.usage); + return C.AUDIOFOCUS_NONE; + } + } + + // Internal audio focus listener. + + private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener { + @Override + public void onAudioFocusChange(int focusChange) { + // Convert the platform focus change to internal state. + switch (focusChange) { + case AudioManager.AUDIOFOCUS_LOSS: + audioFocusState = AUDIO_FOCUS_STATE_LOST_FOCUS; + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT; + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + if (willPauseWhenDucked()) { + audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT; + } else { + audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK; + } + break; + case AudioManager.AUDIOFOCUS_GAIN: + audioFocusState = AUDIO_FOCUS_STATE_HAVE_FOCUS; + break; + default: + Log.w(TAG, "Unknown focus change type: " + focusChange); + // Early return. + return; + } + + // Handle the internal state (change). + switch (audioFocusState) { + case AUDIO_FOCUS_STATE_NO_FOCUS: + // Focus was not requested; nothing to do. + break; + case AUDIO_FOCUS_STATE_LOST_FOCUS: + playerControl.executePlayerCommand(PLAYER_COMMAND_DO_NOT_PLAY); + abandonAudioFocus(/* forceAbandon= */ true); + break; + case AUDIO_FOCUS_STATE_LOSS_TRANSIENT: + playerControl.executePlayerCommand(PLAYER_COMMAND_WAIT_FOR_CALLBACK); + break; + case AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK: + // Volume will be adjusted by the code below. + break; + case AUDIO_FOCUS_STATE_HAVE_FOCUS: + playerControl.executePlayerCommand(PLAYER_COMMAND_PLAY_WHEN_READY); + break; + default: + throw new IllegalStateException("Unknown audio focus state: " + audioFocusState); + } + + float volumeMultiplier = + (audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK) + ? AudioFocusManager.VOLUME_MULTIPLIER_DUCK + : AudioFocusManager.VOLUME_MULTIPLIER_DEFAULT; + if (AudioFocusManager.this.volumeMultiplier != volumeMultiplier) { + AudioFocusManager.this.volumeMultiplier = volumeMultiplier; + playerControl.setVolumeMultiplier(volumeMultiplier); + } + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioListener.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioListener.java new file mode 100644 index 0000000000..8ce365b283 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioListener.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.audio; + +/** A listener for changes in audio configuration. */ +public interface AudioListener { + + /** + * Called when the audio session is set. + * + * @param audioSessionId The audio session id. + */ + default void onAudioSessionId(int audioSessionId) {} + + /** + * Called when the audio attributes change. + * + * @param audioAttributes The audio attributes. + */ + default void onAudioAttributesChanged(AudioAttributes audioAttributes) {} + + /** + * Called when the volume changes. + * + * @param volume The new volume, with 0 being silence and 1 being unity gain. + */ + default void onVolumeChanged(float volume) {} +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index 07584d575e..0db52daa12 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -283,11 +283,12 @@ public interface AudioSink { */ void setAudioAttributes(AudioAttributes audioAttributes); - /** - * Sets the audio session id. - */ + /** Sets the audio session id. */ void setAudioSessionId(int audioSessionId); + /** Sets the auxiliary effect. */ + void setAuxEffectInfo(AuxEffectInfo auxEffectInfo); + /** * Enables tunneling, if possible. The sink is reset if tunneling was previously disabled or if * the audio session id has changed. Enabling tunneling is only possible if the sink is based on a diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java index 4714db8902..0095001299 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java @@ -15,10 +15,13 @@ */ package com.google.android.exoplayer2.audio; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.media.AudioTimestamp; import android.media.AudioTrack; import android.os.SystemClock; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -128,10 +131,10 @@ import java.lang.reflect.Method; private final Listener listener; private final long[] playheadOffsets; - private AudioTrack audioTrack; + private @Nullable AudioTrack audioTrack; private int outputPcmFrameSize; private int bufferSize; - private AudioTimestampPoller audioTimestampPoller; + private @Nullable AudioTimestampPoller audioTimestampPoller; private int outputSampleRate; private boolean needsPassthroughWorkarounds; private long bufferSizeUs; @@ -139,7 +142,7 @@ import java.lang.reflect.Method; private long smoothedPlayheadOffsetUs; private long lastPlayheadSampleTimeUs; - private Method getLatencyMethod; + private @Nullable Method getLatencyMethod; private long latencyUs; private boolean hasData; @@ -193,7 +196,7 @@ import java.lang.reflect.Method; audioTimestampPoller = new AudioTimestampPoller(audioTrack); outputSampleRate = audioTrack.getSampleRate(); needsPassthroughWorkarounds = needsPassthroughWorkarounds(outputEncoding); - isOutputPcm = Util.isEncodingPcm(outputEncoding); + isOutputPcm = Util.isEncodingLinearPcm(outputEncoding); bufferSizeUs = isOutputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET; lastRawPlaybackHeadPosition = 0; rawPlaybackHeadWrapCount = 0; @@ -205,13 +208,14 @@ import java.lang.reflect.Method; } public long getCurrentPositionUs(boolean sourceEnded) { - if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) { + if (Assertions.checkNotNull(this.audioTrack).getPlayState() == PLAYSTATE_PLAYING) { maybeSampleSyncParams(); } // If the device supports it, use the playback timestamp from AudioTrack.getTimestamp. // Otherwise, derive a smoothed position by sampling the track's frame position. long systemTimeUs = System.nanoTime() / 1000; + AudioTimestampPoller audioTimestampPoller = Assertions.checkNotNull(this.audioTimestampPoller); if (audioTimestampPoller.hasTimestamp()) { // Calculate the speed-adjusted position using the timestamp (which may be in the future). long timestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames(); @@ -241,12 +245,12 @@ import java.lang.reflect.Method; /** Starts position tracking. Must be called immediately before {@link AudioTrack#play()}. */ public void start() { - audioTimestampPoller.reset(); + Assertions.checkNotNull(audioTimestampPoller).reset(); } /** Returns whether the audio track is in the playing state. */ public boolean isPlaying() { - return audioTrack.getPlayState() == PLAYSTATE_PLAYING; + return Assertions.checkNotNull(audioTrack).getPlayState() == PLAYSTATE_PLAYING; } /** @@ -257,7 +261,7 @@ import java.lang.reflect.Method; * @return Whether the caller can write data to the track. */ public boolean mayHandleBuffer(long writtenFrames) { - @PlayState int playState = audioTrack.getPlayState(); + @PlayState int playState = Assertions.checkNotNull(audioTrack).getPlayState(); if (needsPassthroughWorkarounds) { // An AC-3 audio track continues to play data written while it is paused. Stop writing so its // buffer empties. See [Internal: b/18899620]. @@ -339,7 +343,7 @@ import java.lang.reflect.Method; if (stopTimestampUs == C.TIME_UNSET) { // The audio track is going to be paused, so reset the timestamp poller to ensure it doesn't // supply an advancing position. - audioTimestampPoller.reset(); + Assertions.checkNotNull(audioTimestampPoller).reset(); return true; } // We've handled the end of the stream already, so there's no need to pause the track. @@ -388,6 +392,7 @@ import java.lang.reflect.Method; } private void maybePollAndCheckTimestamp(long systemTimeUs, long playbackPositionUs) { + AudioTimestampPoller audioTimestampPoller = Assertions.checkNotNull(this.audioTimestampPoller); if (!audioTimestampPoller.maybePollTimestamp(systemTimeUs)) { return; } @@ -423,7 +428,9 @@ import java.lang.reflect.Method; // Compute the audio track latency, excluding the latency due to the buffer (leaving // latency due to the mixer and audio hardware driver). latencyUs = - (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L - bufferSizeUs; + castNonNull((Integer) getLatencyMethod.invoke(Assertions.checkNotNull(audioTrack))) + * 1000L + - bufferSizeUs; // Sanity check that the latency is non-negative. latencyUs = Math.max(latencyUs, 0); // Sanity check that the latency isn't too large. @@ -457,7 +464,7 @@ import java.lang.reflect.Method; */ private boolean forceHasPendingData() { return needsPassthroughWorkarounds - && audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PAUSED + && Assertions.checkNotNull(audioTrack).getPlayState() == AudioTrack.PLAYSTATE_PAUSED && getPlaybackHeadPosition() == 0; } @@ -483,6 +490,7 @@ import java.lang.reflect.Method; * @return The playback head position, in frames. */ private long getPlaybackHeadPosition() { + AudioTrack audioTrack = Assertions.checkNotNull(this.audioTrack); if (stopTimestampUs != C.TIME_UNSET) { // Simulate the playback head position up to the total number of frames submitted. long elapsedTimeSinceStopUs = (SystemClock.elapsedRealtime() * 1000) - stopTimestampUs; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AuxEffectInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AuxEffectInfo.java new file mode 100644 index 0000000000..7462a9c4b0 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AuxEffectInfo.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.audio; + +import android.media.AudioTrack; +import android.media.audiofx.AudioEffect; +import android.support.annotation.Nullable; + +/** + * Represents auxiliary effect information, which can be used to attach an auxiliary effect to an + * underlying {@link AudioTrack}. + * + *

    Auxiliary effects can only be applied if the application has the {@code + * android.permission.MODIFY_AUDIO_SETTINGS} permission. Apps are responsible for retaining the + * associated audio effect instance and releasing it when it's no longer needed. See the + * documentation of {@link AudioEffect} for more information. + */ +public final class AuxEffectInfo { + + /** Value for {@link #effectId} representing no auxiliary effect. */ + public static final int NO_AUX_EFFECT_ID = 0; + + /** + * The identifier of the effect, or {@link #NO_AUX_EFFECT_ID} if there is no effect. + * + * @see android.media.AudioTrack#attachAuxEffect(int) + */ + public final int effectId; + /** + * The send level for the effect. + * + * @see android.media.AudioTrack#setAuxEffectSendLevel(float) + */ + public final float sendLevel; + + /** + * Creates an instance with the given effect identifier and send level. + * + * @param effectId The effect identifier. This is the value returned by {@link + * AudioEffect#getId()} on the effect, or {@value NO_AUX_EFFECT_ID} which represents no + * effect. This value is passed to {@link AudioTrack#attachAuxEffect(int)} on the underlying + * audio track. + * @param sendLevel The send level for the effect, where 0 represents no effect and a value of 1 + * is full send. If {@code effectId} is not {@value #NO_AUX_EFFECT_ID}, this value is passed + * to {@link AudioTrack#setAuxEffectSendLevel(float)} on the underlying audio track. + */ + public AuxEffectInfo(int effectId, float sendLevel) { + this.effectId = effectId; + this.sendLevel = sendLevel; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AuxEffectInfo auxEffectInfo = (AuxEffectInfo) o; + return effectId == auxEffectInfo.effectId + && Float.compare(auxEffectInfo.sendLevel, sendLevel) == 0; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + effectId; + result = 31 * result + Float.floatToIntBits(sendLevel); + return result; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 1025cb953b..aed4c63c75 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -26,6 +26,7 @@ import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -279,6 +280,7 @@ public final class DefaultAudioSink implements AudioSink { private boolean playing; private int audioSessionId; + private AuxEffectInfo auxEffectInfo; private boolean tunneling; private long lastFeedElapsedRealtimeMs; @@ -355,6 +357,7 @@ public final class DefaultAudioSink implements AudioSink { startMediaTimeState = START_NOT_SET; audioAttributes = AudioAttributes.DEFAULT; audioSessionId = C.AUDIO_SESSION_ID_UNSET; + auxEffectInfo = new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, 0f); playbackParameters = PlaybackParameters.DEFAULT; drainingAudioProcessorIndex = C.INDEX_UNSET; activeAudioProcessors = new AudioProcessor[0]; @@ -371,7 +374,7 @@ public final class DefaultAudioSink implements AudioSink { @Override public boolean isEncodingSupported(@C.Encoding int encoding) { - if (Util.isEncodingPcm(encoding)) { + if (Util.isEncodingLinearPcm(encoding)) { // AudioTrack supports 16-bit integer PCM output in all platform API versions, and float // output from platform API version 21 only. Other integer PCM encodings are resampled by this // sink to 16-bit PCM. @@ -405,7 +408,7 @@ public final class DefaultAudioSink implements AudioSink { this.inputSampleRate = inputSampleRate; int channelCount = inputChannelCount; int sampleRate = inputSampleRate; - isInputPcm = Util.isEncodingPcm(inputEncoding); + isInputPcm = Util.isEncodingLinearPcm(inputEncoding); shouldConvertHighResIntPcmToFloat = enableConvertHighResIntPcmToFloat && isEncodingSupported(C.ENCODING_PCM_32BIT) @@ -416,6 +419,16 @@ public final class DefaultAudioSink implements AudioSink { @C.Encoding int encoding = inputEncoding; boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT; canApplyPlaybackParameters = processingEnabled && !shouldConvertHighResIntPcmToFloat; + + if (Util.SDK_INT < 21 && channelCount == 8 && outputChannels == null) { + // AudioTrack doesn't support 8 channel output before Android L. Discard the last two (side) + // channels to give a 6 channel stream that is supported. + outputChannels = new int[6]; + for (int i = 0; i < outputChannels.length; i++) { + outputChannels[i] = i; + } + } + if (processingEnabled) { trimmingAudioProcessor.setTrimFrameCount(trimStartFrames, trimEndFrames); channelMappingAudioProcessor.setChannelMap(outputChannels); @@ -433,55 +446,9 @@ public final class DefaultAudioSink implements AudioSink { } } - int channelConfig; - switch (channelCount) { - case 1: - channelConfig = AudioFormat.CHANNEL_OUT_MONO; - break; - case 2: - channelConfig = AudioFormat.CHANNEL_OUT_STEREO; - break; - case 3: - channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER; - break; - case 4: - channelConfig = AudioFormat.CHANNEL_OUT_QUAD; - break; - case 5: - channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER; - break; - case 6: - channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; - break; - case 7: - channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER; - break; - case 8: - channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; - break; - default: - throw new ConfigurationException("Unsupported channel count: " + channelCount); - } - - // Workaround for overly strict channel configuration checks on nVidia Shield. - if (Util.SDK_INT <= 23 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER)) { - switch (channelCount) { - case 7: - channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; - break; - case 3: - case 5: - channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; - break; - default: - break; - } - } - - // Workaround for Nexus Player not reporting support for mono passthrough. - // (See [Internal: b/34268671].) - if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && !isInputPcm && channelCount == 1) { - channelConfig = AudioFormat.CHANNEL_OUT_STEREO; + int channelConfig = getChannelConfig(channelCount, isInputPcm); + if (channelConfig == AudioFormat.CHANNEL_INVALID) { + throw new ConfigurationException("Unsupported channel count: " + channelCount); } if (!flush @@ -501,29 +468,22 @@ public final class DefaultAudioSink implements AudioSink { outputEncoding = encoding; outputPcmFrameSize = isInputPcm ? Util.getPcmFrameSize(outputEncoding, channelCount) : C.LENGTH_UNSET; - if (specifiedBufferSize != 0) { - bufferSize = specifiedBufferSize; - } else if (isInputPcm) { - int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding); + bufferSize = specifiedBufferSize != 0 ? specifiedBufferSize : getDefaultBufferSize(); + } + + private int getDefaultBufferSize() { + if (isInputPcm) { + int minBufferSize = + AudioTrack.getMinBufferSize(outputSampleRate, outputChannelConfig, outputEncoding); Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize; int maxAppBufferSize = (int) Math.max(minBufferSize, durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize); - bufferSize = Util.constrainValue(multipliedBufferSize, minAppBufferSize, maxAppBufferSize); + return Util.constrainValue(multipliedBufferSize, minAppBufferSize, maxAppBufferSize); } else { - // TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into - // account. [Internal: b/25181305] - if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) { - // AC-3 allows bitrates up to 640 kbit/s. - bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND); - } else if (outputEncoding == C.ENCODING_DTS) { - // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. - bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND); - } else /* outputEncoding == C.ENCODING_DTS_HD || outputEncoding == C.ENCODING_DOLBY_TRUEHD*/ { - // HD passthrough requires a larger buffer to avoid underrun. - bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 6 * 1024 / C.MICROS_PER_SECOND); - } + int rate = getMaximumEncodedRateBytesPerSecond(outputEncoding); + return (int) (PASSTHROUGH_BUFFER_DURATION_US * rate / C.MICROS_PER_SECOND); } } @@ -589,6 +549,11 @@ public final class DefaultAudioSink implements AudioSink { audioTrackPositionTracker.setAudioTrack( audioTrack, outputEncoding, outputPcmFrameSize, bufferSize); setVolumeInternal(); + + if (auxEffectInfo.effectId != AuxEffectInfo.NO_AUX_EFFECT_ID) { + audioTrack.attachAuxEffect(auxEffectInfo.effectId); + audioTrack.setAuxEffectSendLevel(auxEffectInfo.sendLevel); + } } @Override @@ -909,6 +874,24 @@ public final class DefaultAudioSink implements AudioSink { } } + @Override + public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) { + if (this.auxEffectInfo.equals(auxEffectInfo)) { + return; + } + int effectId = auxEffectInfo.effectId; + float sendLevel = auxEffectInfo.sendLevel; + if (audioTrack != null) { + if (this.auxEffectInfo.effectId != effectId) { + audioTrack.attachAuxEffect(effectId); + } + if (effectId != AuxEffectInfo.NO_AUX_EFFECT_ID) { + audioTrack.setAuxEffectSendLevel(sendLevel); + } + } + this.auxEffectInfo = auxEffectInfo; + } + @Override public void enableTunnelingV21(int tunnelingAudioSessionId) { Assertions.checkState(Util.SDK_INT >= 21); @@ -1170,6 +1153,55 @@ public final class DefaultAudioSink implements AudioSink { : toIntPcmAvailableAudioProcessors; } + private static int getChannelConfig(int channelCount, boolean isInputPcm) { + if (Util.SDK_INT <= 28 && !isInputPcm) { + // In passthrough mode the channel count used to configure the audio track doesn't affect how + // the stream is handled, except that some devices do overly-strict channel configuration + // checks. Therefore we override the channel count so that a known-working channel + // configuration is chosen in all cases. See [Internal: b/29116190]. + if (channelCount == 7) { + channelCount = 8; + } else if (channelCount == 3 || channelCount == 4 || channelCount == 5) { + channelCount = 6; + } + } + + // Workaround for Nexus Player not reporting support for mono passthrough. + // (See [Internal: b/34268671].) + if (Util.SDK_INT <= 26 && "fugu".equals(Util.DEVICE) && !isInputPcm && channelCount == 1) { + channelCount = 2; + } + + return Util.getAudioTrackChannelConfig(channelCount); + } + + private static int getMaximumEncodedRateBytesPerSecond(@C.Encoding int encoding) { + switch (encoding) { + case C.ENCODING_AC3: + return 640 * 1000 / 8; + case C.ENCODING_E_AC3: + return 6144 * 1000 / 8; + case C.ENCODING_DTS: + // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. + return 1536 * 1000 / 8; + case C.ENCODING_DTS_HD: + return 18000 * 1000 / 8; + case C.ENCODING_DOLBY_TRUEHD: + return 24500 * 1000 / 8; + case C.ENCODING_INVALID: + case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_24BIT: + case C.ENCODING_PCM_32BIT: + case C.ENCODING_PCM_8BIT: + case C.ENCODING_PCM_A_LAW: + case C.ENCODING_PCM_FLOAT: + case C.ENCODING_PCM_MU_LAW: + case Format.NO_VALUE: + default: + throw new IllegalArgumentException(); + } + } + private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) { if (encoding == C.ENCODING_DTS || encoding == C.ENCODING_DTS_HD) { return DtsUtil.parseDtsAudioSampleCount(buffer); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java index dc07b1a646..f65dc3fc4e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java @@ -74,13 +74,13 @@ public final class DtsUtil { * subsections 5.3/5.4. * * @param frame The DTS frame to parse. - * @param trackId The track identifier to set on the format, or null. + * @param trackId The track identifier to set on the format. * @param language The language to set on the format. * @param drmInitData {@link DrmInitData} to be included in the format. * @return The DTS format parsed from data in the header. */ - public static Format parseDtsFormat(byte[] frame, String trackId, String language, - DrmInitData drmInitData) { + public static Format parseDtsFormat( + byte[] frame, String trackId, String language, DrmInitData drmInitData) { ParsableBitArray frameBits = getNormalizedFrameHeader(frame); frameBits.skipBits(32 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE int amode = frameBits.readBits(6); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 9ab066ee7d..1197cb5a71 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -45,6 +45,8 @@ import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; /** * Decodes and renders audio using {@link MediaCodec} and an {@link AudioSink}. @@ -58,6 +60,9 @@ import java.nio.ByteBuffer; *

  • Message with type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to set the audio attributes. The * message payload should be an {@link com.google.android.exoplayer2.audio.AudioAttributes} * instance that will configure the underlying audio track. + *
  • Message with type {@link C#MSG_SET_AUX_EFFECT_INFO} to set the auxiliary effect. The + * message payload should be an {@link AuxEffectInfo} instance that will configure the + * underlying audio track. *
*/ @TargetApi(16) @@ -71,8 +76,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private boolean passthroughEnabled; private boolean codecNeedsDiscardChannelsWorkaround; private android.media.MediaFormat passthroughMediaFormat; - @C.Encoding - private int pcmEncoding; + private @C.Encoding int pcmEncoding; private int channelCount; private int encoderDelay; private int encoderPadding; @@ -229,7 +233,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) { - super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); + super( + C.TRACK_TYPE_AUDIO, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + /* assumedMinimumCodecOperatingRate= */ 44100); this.context = context.getApplicationContext(); this.audioSink = audioSink; eventDispatcher = new EventDispatcher(eventHandler, eventListener); @@ -262,35 +271,41 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption; } } - MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, - requiresSecureDecryption); - if (decoderInfo == null) { - return requiresSecureDecryption && mediaCodecSelector.getDecoderInfo(mimeType, false) != null - ? FORMAT_UNSUPPORTED_DRM : FORMAT_UNSUPPORTED_SUBTYPE; + List decoderInfos = + mediaCodecSelector.getDecoderInfos(format.sampleMimeType, requiresSecureDecryption); + if (decoderInfos.isEmpty()) { + return requiresSecureDecryption + && !mediaCodecSelector + .getDecoderInfos(format.sampleMimeType, /* requiresSecureDecoder= */ false) + .isEmpty() + ? FORMAT_UNSUPPORTED_DRM + : FORMAT_UNSUPPORTED_SUBTYPE; } if (!supportsFormatDrm) { return FORMAT_UNSUPPORTED_DRM; } - // Note: We assume support for unknown sampleRate and channelCount. - boolean decoderCapable = Util.SDK_INT < 21 - || ((format.sampleRate == Format.NO_VALUE - || decoderInfo.isAudioSampleRateSupportedV21(format.sampleRate)) - && (format.channelCount == Format.NO_VALUE - || decoderInfo.isAudioChannelCountSupportedV21(format.channelCount))); - int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; - return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport; + // Check capabilities for the first decoder in the list, which takes priority. + MediaCodecInfo decoderInfo = decoderInfos.get(0); + boolean isFormatSupported = decoderInfo.isFormatSupported(format); + int adaptiveSupport = + isFormatSupported && decoderInfo.isSeamlessAdaptationSupported(format) + ? ADAPTIVE_SEAMLESS + : ADAPTIVE_NOT_SEAMLESS; + int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; + return adaptiveSupport | tunnelingSupport | formatSupport; } @Override - protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector, - Format format, boolean requiresSecureDecoder) throws DecoderQueryException { + protected List getDecoderInfos( + MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder) + throws DecoderQueryException { if (allowPassthrough(format.sampleMimeType)) { MediaCodecInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo(); if (passthroughDecoderInfo != null) { - return passthroughDecoderInfo; + return Collections.singletonList(passthroughDecoderInfo); } } - return super.getDecoderInfo(mediaCodecSelector, format, requiresSecureDecoder); + return super.getDecoderInfos(mediaCodecSelector, format, requiresSecureDecoder); } /** @@ -307,13 +322,18 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, - MediaCrypto crypto) { + protected void configureCodec( + MediaCodecInfo codecInfo, + MediaCodec codec, + Format format, + MediaCrypto crypto, + float codecOperatingRate) { codecMaxInputSize = getCodecMaxInputSize(codecInfo, format, getStreamFormats()); codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); passthroughEnabled = codecInfo.passthrough; String codecMimeType = codecInfo.mimeType == null ? MimeTypes.AUDIO_RAW : codecInfo.mimeType; - MediaFormat mediaFormat = getMediaFormat(format, codecMimeType, codecMaxInputSize); + MediaFormat mediaFormat = + getMediaFormat(format, codecMimeType, codecMaxInputSize, codecOperatingRate); codec.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0); if (passthroughEnabled) { // Store the input MIME type if we're using the passthrough codec. @@ -327,13 +347,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected @KeepCodecResult int canKeepCodec( MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { - return KEEP_CODEC_RESULT_NO; - // TODO: Determine when codecs can be safely kept. When doing so, also uncomment the commented - // out code in getCodecMaxInputSize. - // return getCodecMaxInputSize(codecInfo, newFormat) <= codecMaxInputSize - // && areAdaptationCompatible(oldFormat, newFormat) - // ? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION - // : KEEP_CODEC_RESULT_NO; + if (getCodecMaxInputSize(codecInfo, newFormat) <= codecMaxInputSize + && codecInfo.isSeamlessAdaptationSupported(oldFormat, newFormat) + && oldFormat.encoderDelay == 0 + && oldFormat.encoderPadding == 0 + && newFormat.encoderDelay == 0 + && newFormat.encoderPadding == 0) { + return KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION; + } else { + return KEEP_CODEC_RESULT_NO; + } } @Override @@ -341,6 +364,21 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return this; } + @Override + protected float getCodecOperatingRate( + float operatingRate, Format format, Format[] streamFormats) { + // Use the highest known stream sample-rate up front, to avoid having to reconfigure the codec + // should an adaptive switch to that stream occur. + int maxSampleRate = -1; + for (Format streamFormat : streamFormats) { + int streamSampleRate = streamFormat.sampleRate; + if (streamSampleRate != Format.NO_VALUE) { + maxSampleRate = Math.max(maxSampleRate, streamSampleRate); + } + } + return maxSampleRate == -1 ? CODEC_OPERATING_RATE_UNSET : (maxSampleRate * operatingRate); + } + @Override protected void onCodecInitialized(String name, long initializedTimestampMs, long initializationDurationMs) { @@ -556,6 +594,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media AudioAttributes audioAttributes = (AudioAttributes) message; audioSink.setAudioAttributes(audioAttributes); break; + case C.MSG_SET_AUX_EFFECT_INFO: + AuxEffectInfo auxEffectInfo = (AuxEffectInfo) message; + audioSink.setAuxEffectInfo(auxEffectInfo); + break; default: super.handleMessage(messageType, message); break; @@ -574,16 +616,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media protected int getCodecMaxInputSize( MediaCodecInfo codecInfo, Format format, Format[] streamFormats) { int maxInputSize = getCodecMaxInputSize(codecInfo, format); - // if (streamFormats.length == 1) { - // // The single entry in streamFormats must correspond to the format for which the codec is - // // being configured. - // return maxInputSize; - // } - // for (Format streamFormat : streamFormats) { - // if (areAdaptationCompatible(format, streamFormat)) { - // maxInputSize = Math.max(maxInputSize, getCodecMaxInputSize(codecInfo, streamFormat)); - // } - // } + if (streamFormats.length == 1) { + // The single entry in streamFormats must correspond to the format for which the codec is + // being configured. + return maxInputSize; + } + for (Format streamFormat : streamFormats) { + if (codecInfo.isSeamlessAdaptationSupported(format, streamFormat)) { + maxInputSize = Math.max(maxInputSize, getCodecMaxInputSize(codecInfo, streamFormat)); + } + } return maxInputSize; } @@ -624,10 +666,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param format The format of the media. * @param codecMimeType The MIME type handled by the codec. * @param codecMaxInputSize The maximum input size supported by the codec. + * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if + * no codec operating rate should be set. * @return The framework media format. */ @SuppressLint("InlinedApi") - protected MediaFormat getMediaFormat(Format format, String codecMimeType, int codecMaxInputSize) { + protected MediaFormat getMediaFormat( + Format format, String codecMimeType, int codecMaxInputSize, float codecOperatingRate) { MediaFormat mediaFormat = new MediaFormat(); // Set format parameters that should always be set. mediaFormat.setString(MediaFormat.KEY_MIME, codecMimeType); @@ -639,6 +684,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media // Set codec configuration values. if (Util.SDK_INT >= 23) { mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */); + if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET) { + mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate); + } } return mediaFormat; } @@ -654,25 +702,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } - /** - * Returns whether a codec with suitable maximum input size will support adaptation between two - * {@link Format}s. - * - * @param first The first format. - * @param second The second format. - * @return Whether the codec will support adaptation between the two {@link Format}s. - */ - private static boolean areAdaptationCompatible(Format first, Format second) { - return first.sampleMimeType.equals(second.sampleMimeType) - && first.channelCount == second.channelCount - && first.sampleRate == second.sampleRate - && first.encoderDelay == 0 - && first.encoderPadding == 0 - && second.encoderDelay == 0 - && second.encoderPadding == 0 - && first.initializationDataEquals(second); - } - /** * Returns whether the decoder is known to output six audio channels when provided with input with * fewer than six channels. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java index a289ced128..96400cd70b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java @@ -33,12 +33,12 @@ public final class SilenceSkippingAudioProcessor implements AudioProcessor { * The minimum duration of audio that must be below {@link #SILENCE_THRESHOLD_LEVEL} to classify * that part of audio as silent, in microseconds. */ - private static final long MINIMUM_SILENCE_DURATION_US = 100_000; + private static final long MINIMUM_SILENCE_DURATION_US = 150_000; /** * The duration of silence by which to extend non-silent sections, in microseconds. The value must * not exceed {@link #MINIMUM_SILENCE_DURATION_US}. */ - private static final long PADDING_SILENCE_US = 10_000; + private static final long PADDING_SILENCE_US = 20_000; /** * The absolute level below which an individual PCM sample is classified as silent. Note: the * specified value will be rounded so that the threshold check only depends on the more diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index c404912882..83b14c071d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -20,6 +20,7 @@ import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -57,6 +58,9 @@ import java.lang.annotation.RetentionPolicy; *
  • Message with type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to set the audio attributes. The * message payload should be an {@link com.google.android.exoplayer2.audio.AudioAttributes} * instance that will configure the underlying audio track. + *
  • Message with type {@link C#MSG_SET_AUX_EFFECT_INFO} to set the auxiliary effect. The + * message payload should be an {@link AuxEffectInfo} instance that will configure the + * underlying audio track. * */ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock { @@ -121,7 +125,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ - public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, + public SimpleDecoderAudioRenderer( + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) { this( eventHandler, @@ -139,8 +145,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. */ - public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities) { + public SimpleDecoderAudioRenderer( + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + @Nullable AudioCapabilities audioCapabilities) { this( eventHandler, eventListener, @@ -164,9 +172,13 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * has obtained the keys necessary to decrypt encrypted regions of the media. * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ - public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, AudioProcessor... audioProcessors) { + public SimpleDecoderAudioRenderer( + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + @Nullable AudioCapabilities audioCapabilities, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + AudioProcessor... audioProcessors) { this(eventHandler, eventListener, drmSessionManager, playClearSamplesWithoutKeys, new DefaultAudioSink(audioCapabilities, audioProcessors)); } @@ -184,8 +196,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * has obtained the keys necessary to decrypt encrypted regions of the media. * @param audioSink The sink to which audio will be output. */ - public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + public SimpleDecoderAudioRenderer( + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, AudioSink audioSink) { super(C.TRACK_TYPE_AUDIO); this.drmSessionManager = drmSessionManager; @@ -206,6 +221,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override public final int supportsFormat(Format format) { + if (!MimeTypes.isAudio(format.sampleMimeType)) { + return FORMAT_UNSUPPORTED_TYPE; + } int formatSupport = supportsFormatInternal(drmSessionManager, format); if (formatSupport <= FORMAT_UNSUPPORTED_DRM) { return formatSupport; @@ -215,15 +233,15 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } /** - * Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for - * {@link #supportsFormat(Format)}. + * Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for {@link + * #supportsFormat(Format)}. * * @param drmSessionManager The renderer's {@link DrmSessionManager}. - * @param format The format. + * @param format The format, which has an audio {@link Format#sampleMimeType}. * @return The extent to which the renderer supports the format itself. */ - protected abstract int supportsFormatInternal(DrmSessionManager drmSessionManager, - Format format); + protected abstract int supportsFormatInternal( + DrmSessionManager drmSessionManager, Format format); /** * Returns whether the audio sink can accept audio in the specified encoding. @@ -575,6 +593,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements AudioAttributes audioAttributes = (AudioAttributes) message; audioSink.setAudioAttributes(audioAttributes); break; + case C.MSG_SET_AUX_EFFECT_INFO: + AuxEffectInfo auxEffectInfo = (AuxEffectInfo) message; + audioSink.setAuxEffectInfo(auxEffectInfo); + break; default: super.handleMessage(messageType, message); break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java new file mode 100644 index 0000000000..654d4edc56 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.audio; + +import android.support.annotation.Nullable; +import android.util.Log; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Audio processor that outputs its input unmodified and also outputs its input to a given sink. + * This is intended to be used for diagnostics and debugging. + * + *

    This audio processor can be inserted into the audio processor chain to access audio data + * before/after particular processing steps have been applied. For example, to get audio output + * after playback speed adjustment and silence skipping have been applied it is necessary to pass a + * custom {@link com.google.android.exoplayer2.audio.DefaultAudioSink.AudioProcessorChain} when + * creating the audio sink, and include this audio processor after all other audio processors. + */ +public final class TeeAudioProcessor implements AudioProcessor { + + /** A sink for audio buffers handled by the audio processor. */ + public interface AudioBufferSink { + + /** Called when the audio processor is flushed with a format of subsequent input. */ + void flush(int sampleRateHz, int channelCount, @C.Encoding int encoding); + + /** + * Called when data is written to the audio processor. + * + * @param buffer A read-only buffer containing input which the audio processor will handle. + */ + void handleBuffer(ByteBuffer buffer); + } + + private final AudioBufferSink audioBufferSink; + + private int sampleRateHz; + private int channelCount; + private @C.Encoding int encoding; + private boolean isActive; + + private ByteBuffer buffer; + private ByteBuffer outputBuffer; + private boolean inputEnded; + + /** + * Creates a new tee audio processor, sending incoming data to the given {@link AudioBufferSink}. + * + * @param audioBufferSink The audio buffer sink that will receive input queued to this audio + * processor. + */ + public TeeAudioProcessor(AudioBufferSink audioBufferSink) { + this.audioBufferSink = Assertions.checkNotNull(audioBufferSink); + + buffer = EMPTY_BUFFER; + outputBuffer = EMPTY_BUFFER; + channelCount = Format.NO_VALUE; + sampleRateHz = Format.NO_VALUE; + } + + @Override + public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) + throws UnhandledFormatException { + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + this.encoding = encoding; + boolean wasActive = isActive; + isActive = true; + return !wasActive; + } + + @Override + public boolean isActive() { + return isActive; + } + + @Override + public int getOutputChannelCount() { + return channelCount; + } + + @Override + public int getOutputEncoding() { + return encoding; + } + + @Override + public int getOutputSampleRateHz() { + return sampleRateHz; + } + + @Override + public void queueInput(ByteBuffer buffer) { + int remaining = buffer.remaining(); + if (remaining == 0) { + return; + } + + audioBufferSink.handleBuffer(buffer.asReadOnlyBuffer()); + + if (this.buffer.capacity() < remaining) { + this.buffer = ByteBuffer.allocateDirect(remaining).order(ByteOrder.nativeOrder()); + } else { + this.buffer.clear(); + } + + this.buffer.put(buffer); + + this.buffer.flip(); + outputBuffer = this.buffer; + } + + @Override + public void queueEndOfStream() { + inputEnded = true; + } + + @Override + public ByteBuffer getOutput() { + ByteBuffer outputBuffer = this.outputBuffer; + this.outputBuffer = EMPTY_BUFFER; + return outputBuffer; + } + + @SuppressWarnings("ReferenceEquality") + @Override + public boolean isEnded() { + return inputEnded && buffer == EMPTY_BUFFER; + } + + @Override + public void flush() { + outputBuffer = EMPTY_BUFFER; + inputEnded = false; + + audioBufferSink.flush(sampleRateHz, channelCount, encoding); + } + + @Override + public void reset() { + flush(); + buffer = EMPTY_BUFFER; + sampleRateHz = Format.NO_VALUE; + channelCount = Format.NO_VALUE; + encoding = Format.NO_VALUE; + } + + /** + * A sink for audio buffers that writes output audio as .wav files with a given path prefix. When + * new audio data is handled after flushing the audio processor, a counter is incremented and its + * value is appended to the output file name. + * + *

    Note: if writing to external storage it's necessary to grant the {@code + * WRITE_EXTERNAL_STORAGE} permission. + */ + public static final class WavFileAudioBufferSink implements AudioBufferSink { + + private static final String TAG = "WaveFileAudioBufferSink"; + + private static final int FILE_SIZE_MINUS_8_OFFSET = 4; + private static final int FILE_SIZE_MINUS_44_OFFSET = 40; + private static final int HEADER_LENGTH = 44; + + private final String outputFileNamePrefix; + private final byte[] scratchBuffer; + private final ByteBuffer scratchByteBuffer; + + private int sampleRateHz; + private int channelCount; + private @C.Encoding int encoding; + private @Nullable RandomAccessFile randomAccessFile; + private int counter; + private int bytesWritten; + + /** + * Creates a new audio buffer sink that writes to .wav files with the given prefix. + * + * @param outputFileNamePrefix The prefix for output files. + */ + public WavFileAudioBufferSink(String outputFileNamePrefix) { + this.outputFileNamePrefix = outputFileNamePrefix; + scratchBuffer = new byte[1024]; + scratchByteBuffer = ByteBuffer.wrap(scratchBuffer).order(ByteOrder.LITTLE_ENDIAN); + } + + @Override + public void flush(int sampleRateHz, int channelCount, int encoding) { + try { + reset(); + } catch (IOException e) { + Log.e(TAG, "Error resetting", e); + } + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + this.encoding = encoding; + } + + @Override + public void handleBuffer(ByteBuffer buffer) { + try { + maybePrepareFile(); + writeBuffer(buffer); + } catch (IOException e) { + Log.e(TAG, "Error writing data", e); + } + } + + private void maybePrepareFile() throws IOException { + if (randomAccessFile != null) { + return; + } + RandomAccessFile randomAccessFile = new RandomAccessFile(getNextOutputFileName(), "rw"); + writeFileHeader(randomAccessFile); + this.randomAccessFile = randomAccessFile; + bytesWritten = HEADER_LENGTH; + } + + private void writeFileHeader(RandomAccessFile randomAccessFile) throws IOException { + // Write the start of the header as big endian data. + randomAccessFile.writeInt(WavUtil.RIFF_FOURCC); + randomAccessFile.writeInt(-1); + randomAccessFile.writeInt(WavUtil.WAVE_FOURCC); + randomAccessFile.writeInt(WavUtil.FMT_FOURCC); + + // Write the rest of the header as little endian data. + scratchByteBuffer.clear(); + scratchByteBuffer.putInt(16); + scratchByteBuffer.putShort((short) WavUtil.getTypeForEncoding(encoding)); + scratchByteBuffer.putShort((short) channelCount); + scratchByteBuffer.putInt(sampleRateHz); + int bytesPerSample = Util.getPcmFrameSize(encoding, channelCount); + scratchByteBuffer.putInt(bytesPerSample * sampleRateHz); + scratchByteBuffer.putShort((short) bytesPerSample); + scratchByteBuffer.putShort((short) (8 * bytesPerSample / channelCount)); + randomAccessFile.write(scratchBuffer, 0, scratchByteBuffer.position()); + + // Write the start of the data chunk as big endian data. + randomAccessFile.writeInt(WavUtil.DATA_FOURCC); + randomAccessFile.writeInt(-1); + } + + private void writeBuffer(ByteBuffer buffer) throws IOException { + RandomAccessFile randomAccessFile = Assertions.checkNotNull(this.randomAccessFile); + while (buffer.hasRemaining()) { + int bytesToWrite = Math.min(buffer.remaining(), scratchBuffer.length); + buffer.get(scratchBuffer, 0, bytesToWrite); + randomAccessFile.write(scratchBuffer, 0, bytesToWrite); + bytesWritten += bytesToWrite; + } + } + + private void reset() throws IOException { + RandomAccessFile randomAccessFile = this.randomAccessFile; + if (randomAccessFile == null) { + return; + } + + try { + scratchByteBuffer.clear(); + scratchByteBuffer.putInt(bytesWritten - 8); + randomAccessFile.seek(FILE_SIZE_MINUS_8_OFFSET); + randomAccessFile.write(scratchBuffer, 0, 4); + + scratchByteBuffer.clear(); + scratchByteBuffer.putInt(bytesWritten - 44); + randomAccessFile.seek(FILE_SIZE_MINUS_44_OFFSET); + randomAccessFile.write(scratchBuffer, 0, 4); + } catch (IOException e) { + // The file may still be playable, so just log a warning. + Log.w(TAG, "Error updating file size", e); + } + + try { + randomAccessFile.close(); + } finally { + this.randomAccessFile = null; + } + } + + private String getNextOutputFileName() { + return Util.formatInvariant("%s-%04d.wav", outputFileNamePrefix, counter++); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java new file mode 100644 index 0000000000..473a91fedf --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.audio; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.Util; + +/** Utilities for handling WAVE files. */ +public final class WavUtil { + + /** Four character code for "RIFF". */ + public static final int RIFF_FOURCC = Util.getIntegerCodeForString("RIFF"); + /** Four character code for "WAVE". */ + public static final int WAVE_FOURCC = Util.getIntegerCodeForString("WAVE"); + /** Four character code for "fmt ". */ + public static final int FMT_FOURCC = Util.getIntegerCodeForString("fmt "); + /** Four character code for "data". */ + public static final int DATA_FOURCC = Util.getIntegerCodeForString("data"); + + /** WAVE type value for integer PCM audio data. */ + private static final int TYPE_PCM = 0x0001; + /** WAVE type value for float PCM audio data. */ + private static final int TYPE_FLOAT = 0x0003; + /** WAVE type value for 8-bit ITU-T G.711 A-law audio data. */ + private static final int TYPE_A_LAW = 0x0006; + /** WAVE type value for 8-bit ITU-T G.711 mu-law audio data. */ + private static final int TYPE_MU_LAW = 0x0007; + /** WAVE type value for extended WAVE format. */ + private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; + + /** Returns the WAVE type value for the given {@code encoding}. */ + public static int getTypeForEncoding(@C.PcmEncoding int encoding) { + switch (encoding) { + case C.ENCODING_PCM_8BIT: + case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_24BIT: + case C.ENCODING_PCM_32BIT: + return TYPE_PCM; + case C.ENCODING_PCM_A_LAW: + return TYPE_A_LAW; + case C.ENCODING_PCM_MU_LAW: + return TYPE_MU_LAW; + case C.ENCODING_PCM_FLOAT: + return TYPE_FLOAT; + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + throw new IllegalArgumentException(); + } + } + + /** Returns the PCM encoding for the given WAVE {@code type} value. */ + public static @C.PcmEncoding int getEncodingForType(int type, int bitsPerSample) { + switch (type) { + case TYPE_PCM: + case TYPE_WAVE_FORMAT_EXTENSIBLE: + return Util.getPcmEncoding(bitsPerSample); + case TYPE_FLOAT: + return bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID; + case TYPE_A_LAW: + return C.ENCODING_PCM_A_LAW; + case TYPE_MU_LAW: + return C.ENCODING_PCM_MU_LAW; + default: + return C.ENCODING_INVALID; + } + } + + private WavUtil() { + // Prevent instantiation. + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java index 68089d7b41..98b1c7ca0f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java @@ -15,9 +15,10 @@ */ package com.google.android.exoplayer2.decoder; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; -import java.util.LinkedList; +import java.util.ArrayDeque; /** * Base class for {@link Decoder}s that use their own decode thread. @@ -28,8 +29,8 @@ public abstract class SimpleDecoder queuedInputBuffers; - private final LinkedList queuedOutputBuffers; + private final ArrayDeque queuedInputBuffers; + private final ArrayDeque queuedOutputBuffers; private final I[] availableInputBuffers; private final O[] availableOutputBuffers; @@ -48,8 +49,8 @@ public abstract class SimpleDecoder(); - queuedOutputBuffers = new LinkedList<>(); + queuedInputBuffers = new ArrayDeque<>(); + queuedOutputBuffers = new ArrayDeque<>(); availableInputBuffers = inputBuffers; availableInputBufferCount = inputBuffers.length; for (int i = 0; i < availableInputBufferCount; i++) { @@ -142,7 +143,7 @@ public abstract class SimpleDecoder mediaDrm; private final ProvisioningManager provisioningManager; - private final byte[] initData; - private final String mimeType; + private final SchemeData schemeData; private final @DefaultDrmSessionManager.Mode int mode; private final HashMap optionalKeyRequestParameters; - private final EventDispatcher eventDispatcher; + private final EventDispatcher eventDispatcher; private final int initialDrmRequestRetryCount; /* package */ final MediaDrmCallback callback; @@ -97,15 +97,20 @@ import java.util.UUID; private byte[] sessionId; private byte[] offlineLicenseKeySetId; + private Object currentKeyRequest; + private Object currentProvisionRequest; + /** * Instantiates a new DRM session. * * @param uuid The UUID of the drm scheme. * @param mediaDrm The media DRM. * @param provisioningManager The manager for provisioning. - * @param initData The DRM init data. + * @param schemeData The DRM data for this session, or null if a {@code offlineLicenseKeySetId} is + * provided. * @param mode The DRM mode. - * @param offlineLicenseKeySetId The offlineLicense KeySetId. + * @param offlineLicenseKeySetId The offline license key set identifier, or null when not using + * offline keys. * @param optionalKeyRequestParameters The optional key request parameters. * @param callback The media DRM callback. * @param playbackLooper The playback looper. @@ -117,20 +122,20 @@ import java.util.UUID; UUID uuid, ExoMediaDrm mediaDrm, ProvisioningManager provisioningManager, - byte[] initData, - String mimeType, + @Nullable SchemeData schemeData, @DefaultDrmSessionManager.Mode int mode, - byte[] offlineLicenseKeySetId, + @Nullable byte[] offlineLicenseKeySetId, HashMap optionalKeyRequestParameters, MediaDrmCallback callback, Looper playbackLooper, - EventDispatcher eventDispatcher, + EventDispatcher eventDispatcher, int initialDrmRequestRetryCount) { this.uuid = uuid; this.provisioningManager = provisioningManager; this.mediaDrm = mediaDrm; this.mode = mode; this.offlineLicenseKeySetId = offlineLicenseKeySetId; + this.schemeData = offlineLicenseKeySetId == null ? schemeData : null; this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.callback = callback; this.initialDrmRequestRetryCount = initialDrmRequestRetryCount; @@ -141,14 +146,6 @@ import java.util.UUID; requestHandlerThread = new HandlerThread("DrmRequestHandler"); requestHandlerThread.start(); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); - - if (offlineLicenseKeySetId == null) { - this.initData = initData; - this.mimeType = mimeType; - } else { - this.initData = null; - this.mimeType = null; - } } // Life cycle. @@ -177,6 +174,8 @@ import java.util.UUID; requestHandlerThread = null; mediaCrypto = null; lastException = null; + currentKeyRequest = null; + currentProvisionRequest = null; if (sessionId != null) { mediaDrm.closeSession(sessionId); sessionId = null; @@ -187,18 +186,42 @@ import java.util.UUID; } public boolean hasInitData(byte[] initData) { - return Arrays.equals(this.initData, initData); + return Arrays.equals(schemeData != null ? schemeData.data : null, initData); } public boolean hasSessionId(byte[] sessionId) { return Arrays.equals(this.sessionId, sessionId); } + @SuppressWarnings("deprecation") + public void onMediaDrmEvent(int what) { + if (!isOpen()) { + return; + } + switch (what) { + case ExoMediaDrm.EVENT_KEY_REQUIRED: + doLicense(false); + break; + case ExoMediaDrm.EVENT_KEY_EXPIRED: + // When an already expired key is loaded MediaDrm sends this event immediately. Ignore + // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still + // waiting for key response. + onKeysExpired(); + break; + case ExoMediaDrm.EVENT_PROVISION_REQUIRED: + state = STATE_OPENED; + provisioningManager.provisionRequired(this); + break; + default: + break; + } + } + // Provisioning implementation. public void provision() { - ProvisionRequest request = mediaDrm.getProvisionRequest(); - postRequestHandler.obtainMessage(MSG_PROVISION, request, true).sendToTarget(); + currentProvisionRequest = mediaDrm.getProvisionRequest(); + postRequestHandler.post(MSG_PROVISION, currentProvisionRequest, /* allowRetry= */ true); } public void onProvisionCompleted() { @@ -271,11 +294,12 @@ import java.util.UUID; return false; } - private void onProvisionResponse(Object response) { - if (state != STATE_OPENING && !isOpen()) { + private void onProvisionResponse(Object request, Object response) { + if (request != currentProvisionRequest || (state != STATE_OPENING && !isOpen())) { // This event is stale. return; } + currentProvisionRequest = null; if (response instanceof Exception) { provisioningManager.onProvisionError((Exception) response); @@ -309,7 +333,7 @@ import java.util.UUID; onError(new KeysExpiredException()); } else { state = STATE_OPENED_WITH_KEYS; - eventDispatcher.drmKeysRestored(); + eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysRestored); } } break; @@ -356,24 +380,30 @@ import java.util.UUID; private void postKeyRequest(int type, boolean allowRetry) { byte[] scope = type == ExoMediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId; + byte[] initData = null; + String mimeType = null; + String licenseServerUrl = null; + if (schemeData != null) { + initData = schemeData.data; + mimeType = schemeData.mimeType; + licenseServerUrl = schemeData.licenseServerUrl; + } try { - KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type, - optionalKeyRequestParameters); - if (C.CLEARKEY_UUID.equals(uuid)) { - request = new DefaultKeyRequest(ClearKeyUtil.adjustRequestData(request.getData()), - request.getDefaultUrl()); - } - postRequestHandler.obtainMessage(MSG_KEYS, request, allowRetry).sendToTarget(); + KeyRequest mediaDrmKeyRequest = + mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters); + currentKeyRequest = Pair.create(mediaDrmKeyRequest, licenseServerUrl); + postRequestHandler.post(MSG_KEYS, currentKeyRequest, allowRetry); } catch (Exception e) { onKeysError(e); } } - private void onKeyResponse(Object response) { - if (!isOpen()) { + private void onKeyResponse(Object request, Object response) { + if (request != currentKeyRequest || !isOpen()) { // This event is stale. return; } + currentKeyRequest = null; if (response instanceof Exception) { onKeysError((Exception) response); @@ -382,12 +412,9 @@ import java.util.UUID; try { byte[] responseData = (byte[]) response; - if (C.CLEARKEY_UUID.equals(uuid)) { - responseData = ClearKeyUtil.adjustResponseData(responseData); - } if (mode == DefaultDrmSessionManager.MODE_RELEASE) { mediaDrm.provideKeyResponse(offlineLicenseKeySetId, responseData); - eventDispatcher.drmKeysRemoved(); + eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysRestored); } else { byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, responseData); if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD @@ -396,7 +423,7 @@ import java.util.UUID; offlineLicenseKeySetId = keySetId; } state = STATE_OPENED_WITH_KEYS; - eventDispatcher.drmKeysLoaded(); + eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysLoaded); } } catch (Exception e) { onKeysError(e); @@ -420,7 +447,7 @@ import java.util.UUID; private void onError(final Exception e) { lastException = new DrmSessionException(e); - eventDispatcher.drmSessionManagerError(e); + eventDispatcher.dispatch(listener -> listener.onDrmSessionManagerError(e)); if (state != STATE_OPENED_WITH_KEYS) { state = STATE_ERROR; } @@ -430,30 +457,7 @@ import java.util.UUID; return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS; } - @SuppressWarnings("deprecation") - public void onMediaDrmEvent(int what) { - if (!isOpen()) { - return; - } - switch (what) { - case ExoMediaDrm.EVENT_KEY_REQUIRED: - doLicense(false); - break; - case ExoMediaDrm.EVENT_KEY_EXPIRED: - // When an already expired key is loaded MediaDrm sends this event immediately. Ignore - // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still - // waiting for key response. - onKeysExpired(); - break; - case ExoMediaDrm.EVENT_PROVISION_REQUIRED: - state = STATE_OPENED; - provisioningManager.provisionRequired(this); - break; - default: - break; - } - - } + // Internal classes. @SuppressLint("HandlerLeak") private class PostResponseHandler extends Handler { @@ -464,12 +468,15 @@ import java.util.UUID; @Override public void handleMessage(Message msg) { + Pair requestAndResponse = (Pair) msg.obj; + Object request = requestAndResponse.first; + Object response = requestAndResponse.second; switch (msg.what) { case MSG_PROVISION: - onProvisionResponse(msg.obj); + onProvisionResponse(request, response); break; case MSG_KEYS: - onKeyResponse(msg.obj); + onKeyResponse(request, response); break; default: break; @@ -486,21 +493,27 @@ import java.util.UUID; super(backgroundLooper); } - Message obtainMessage(int what, Object object, boolean allowRetry) { - return obtainMessage(what, allowRetry ? 1 : 0 /* allow retry*/, 0 /* error count */, - object); + void post(int what, Object request, boolean allowRetry) { + int allowRetryInt = allowRetry ? 1 : 0; + int errorCount = 0; + obtainMessage(what, allowRetryInt, errorCount, request).sendToTarget(); } @Override + @SuppressWarnings("unchecked") public void handleMessage(Message msg) { + Object request = msg.obj; Object response; try { switch (msg.what) { case MSG_PROVISION: - response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj); + response = callback.executeProvisionRequest(uuid, (ProvisionRequest) request); break; case MSG_KEYS: - response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj); + Pair keyRequest = (Pair) request; + KeyRequest mediaDrmKeyRequest = keyRequest.first; + String licenseServerUrl = keyRequest.second; + response = callback.executeKeyRequest(uuid, mediaDrmKeyRequest, licenseServerUrl); break; default: throw new RuntimeException(); @@ -511,7 +524,7 @@ import java.util.UUID; } response = e; } - postResponseHandler.obtainMessage(msg.what, response).sendToTarget(); + postResponseHandler.obtainMessage(msg.what, Pair.create(request, response)).sendToTarget(); } private boolean maybeRetryRequest(Message originalMsg) { @@ -534,5 +547,4 @@ import java.util.UUID; } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java index 7cdee7c537..afec4b6114 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java @@ -15,10 +15,7 @@ */ package com.google.android.exoplayer2.drm; -import android.os.Handler; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.util.Assertions; -import java.util.concurrent.CopyOnWriteArrayList; /** Listener of {@link DefaultDrmSessionManager} events. */ public interface DefaultDrmSessionEventListener { @@ -45,97 +42,4 @@ public interface DefaultDrmSessionEventListener { /** Called each time offline keys are removed. */ void onDrmKeysRemoved(); - - /** Dispatches drm events to all registered listeners. */ - final class EventDispatcher { - - private final CopyOnWriteArrayList listeners; - - /** Creates event dispatcher. */ - public EventDispatcher() { - listeners = new CopyOnWriteArrayList<>(); - } - - /** Adds listener to event dispatcher. */ - public void addListener(Handler handler, DefaultDrmSessionEventListener eventListener) { - Assertions.checkArgument(handler != null && eventListener != null); - listeners.add(new HandlerAndListener(handler, eventListener)); - } - - /** Removes listener from event dispatcher. */ - public void removeListener(DefaultDrmSessionEventListener eventListener) { - for (HandlerAndListener handlerAndListener : listeners) { - if (handlerAndListener.listener == eventListener) { - listeners.remove(handlerAndListener); - } - } - } - - /** Dispatches {@link DefaultDrmSessionEventListener#onDrmKeysLoaded()}. */ - public void drmKeysLoaded() { - for (HandlerAndListener handlerAndListener : listeners) { - final DefaultDrmSessionEventListener listener = handlerAndListener.listener; - handlerAndListener.handler.post( - new Runnable() { - @Override - public void run() { - listener.onDrmKeysLoaded(); - } - }); - } - } - - /** Dispatches {@link DefaultDrmSessionEventListener#onDrmSessionManagerError(Exception)}. */ - public void drmSessionManagerError(final Exception e) { - for (HandlerAndListener handlerAndListener : listeners) { - final DefaultDrmSessionEventListener listener = handlerAndListener.listener; - handlerAndListener.handler.post( - new Runnable() { - @Override - public void run() { - listener.onDrmSessionManagerError(e); - } - }); - } - } - - /** Dispatches {@link DefaultDrmSessionEventListener#onDrmKeysRestored()}. */ - public void drmKeysRestored() { - for (HandlerAndListener handlerAndListener : listeners) { - final DefaultDrmSessionEventListener listener = handlerAndListener.listener; - handlerAndListener.handler.post( - new Runnable() { - @Override - public void run() { - listener.onDrmKeysRestored(); - } - }); - } - } - - /** Dispatches {@link DefaultDrmSessionEventListener#onDrmKeysRemoved()}. */ - public void drmKeysRemoved() { - for (HandlerAndListener handlerAndListener : listeners) { - final DefaultDrmSessionEventListener listener = handlerAndListener.listener; - handlerAndListener.handler.post( - new Runnable() { - @Override - public void run() { - listener.onDrmKeysRemoved(); - } - }); - } - } - - private static final class HandlerAndListener { - - public final Handler handler; - public final DefaultDrmSessionEventListener listener; - - public HandlerAndListener(Handler handler, DefaultDrmSessionEventListener eventListener) { - this.handler = handler; - this.listener = eventListener; - } - } - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 66c9e5cde7..895c27ad93 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -26,13 +26,12 @@ import android.text.TextUtils; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DefaultDrmSession.ProvisioningManager; -import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener.EventDispatcher; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.EventDispatcher; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -89,13 +88,12 @@ public class DefaultDrmSessionManager implements DrmSe public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3; private static final String TAG = "DefaultDrmSessionMgr"; - private static final String CENC_SCHEME_MIME_TYPE = "cenc"; private final UUID uuid; private final ExoMediaDrm mediaDrm; private final MediaDrmCallback callback; private final HashMap optionalKeyRequestParameters; - private final EventDispatcher eventDispatcher; + private final EventDispatcher eventDispatcher; private final boolean multiSession; private final int initialDrmRequestRetryCount; @@ -356,7 +354,7 @@ public class DefaultDrmSessionManager implements DrmSe this.mediaDrm = mediaDrm; this.callback = callback; this.optionalKeyRequestParameters = optionalKeyRequestParameters; - this.eventDispatcher = new EventDispatcher(); + this.eventDispatcher = new EventDispatcher<>(); this.multiSession = multiSession; this.initialDrmRequestRetryCount = initialDrmRequestRetryCount; mode = MODE_PLAYBACK; @@ -509,17 +507,14 @@ public class DefaultDrmSessionManager implements DrmSe } } - byte[] initData = null; - String mimeType = null; + SchemeData schemeData = null; if (offlineLicenseKeySetId == null) { - SchemeData data = getSchemeData(drmInitData, uuid, false); - if (data == null) { + schemeData = getSchemeData(drmInitData, uuid, false); + if (schemeData == null) { final MissingSchemeDataException error = new MissingSchemeDataException(uuid); - eventDispatcher.drmSessionManagerError(error); + eventDispatcher.dispatch(listener -> listener.onDrmSessionManagerError(error)); return new ErrorStateDrmSession<>(new DrmSessionException(error)); } - initData = getSchemeInitData(data, uuid); - mimeType = getSchemeMimeType(data, uuid); } DefaultDrmSession session; @@ -528,6 +523,7 @@ public class DefaultDrmSessionManager implements DrmSe } else { // Only use an existing session if it has matching init data. session = null; + byte[] initData = schemeData != null ? schemeData.data : null; for (DefaultDrmSession existingSession : sessions) { if (existingSession.hasInitData(initData)) { session = existingSession; @@ -543,8 +539,7 @@ public class DefaultDrmSessionManager implements DrmSe uuid, mediaDrm, this, - initData, - mimeType, + schemeData, mode, offlineLicenseKeySetId, optionalKeyRequestParameters, @@ -650,31 +645,6 @@ public class DefaultDrmSessionManager implements DrmSe return matchingSchemeDatas.get(0); } - private static byte[] getSchemeInitData(SchemeData data, UUID uuid) { - byte[] schemeInitData = data.data; - if (Util.SDK_INT < 21) { - // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. - byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, uuid); - if (psshData == null) { - // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. - } else { - schemeInitData = psshData; - } - } - return schemeInitData; - } - - private static String getSchemeMimeType(SchemeData data, UUID uuid) { - String schemeMimeType = data.mimeType; - if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid) - && (MimeTypes.VIDEO_MP4.equals(schemeMimeType) - || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) { - // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4. - schemeMimeType = CENC_SCHEME_MIME_TYPE; - } - return schemeMimeType; - } - @SuppressLint("HandlerLeak") private class MediaDrmHandler extends Handler { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index c2de662010..b9415c74af 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -266,9 +266,9 @@ public final class DrmInitData implements Comparator, Parcelable { * applies to all schemes). */ private final UUID uuid; - /** - * The mimeType of {@link #data}. - */ + /** The URL of the server to which license requests should be made. May be null if unknown. */ + public final @Nullable String licenseServerUrl; + /** The mimeType of {@link #data}. */ public final String mimeType; /** * The initialization data. May be null for scheme support checks only. @@ -297,7 +297,25 @@ public final class DrmInitData implements Comparator, Parcelable { * @param requiresSecureDecryption See {@link #requiresSecureDecryption}. */ public SchemeData(UUID uuid, String mimeType, byte[] data, boolean requiresSecureDecryption) { + this(uuid, /* licenseServerUrl= */ null, mimeType, data, requiresSecureDecryption); + } + + /** + * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is + * universal (i.e. applies to all schemes). + * @param licenseServerUrl See {@link #licenseServerUrl}. + * @param mimeType See {@link #mimeType}. + * @param data See {@link #data}. + * @param requiresSecureDecryption See {@link #requiresSecureDecryption}. + */ + public SchemeData( + UUID uuid, + @Nullable String licenseServerUrl, + String mimeType, + byte[] data, + boolean requiresSecureDecryption) { this.uuid = Assertions.checkNotNull(uuid); + this.licenseServerUrl = licenseServerUrl; this.mimeType = Assertions.checkNotNull(mimeType); this.data = data; this.requiresSecureDecryption = requiresSecureDecryption; @@ -305,6 +323,7 @@ public final class DrmInitData implements Comparator, Parcelable { /* package */ SchemeData(Parcel in) { uuid = new UUID(in.readLong(), in.readLong()); + licenseServerUrl = in.readString(); mimeType = in.readString(); data = in.createByteArray(); requiresSecureDecryption = in.readByte() != 0; @@ -337,6 +356,16 @@ public final class DrmInitData implements Comparator, Parcelable { return data != null; } + /** + * Returns a copy of this instance with the specified data. + * + * @param data The data to include in the copy. + * @return The new instance. + */ + public SchemeData copyWithData(@Nullable byte[] data) { + return new SchemeData(uuid, licenseServerUrl, mimeType, data, requiresSecureDecryption); + } + @Override public boolean equals(@Nullable Object obj) { if (!(obj instanceof SchemeData)) { @@ -346,7 +375,9 @@ public final class DrmInitData implements Comparator, Parcelable { return true; } SchemeData other = (SchemeData) obj; - return mimeType.equals(other.mimeType) && Util.areEqual(uuid, other.uuid) + return Util.areEqual(licenseServerUrl, other.licenseServerUrl) + && Util.areEqual(mimeType, other.mimeType) + && Util.areEqual(uuid, other.uuid) && Arrays.equals(data, other.data); } @@ -354,6 +385,7 @@ public final class DrmInitData implements Comparator, Parcelable { public int hashCode() { if (hashCode == 0) { int result = uuid.hashCode(); + result = 31 * result + (licenseServerUrl == null ? 0 : licenseServerUrl.hashCode()); result = 31 * result + mimeType.hashCode(); result = 31 * result + Arrays.hashCode(data); hashCode = result; @@ -372,6 +404,7 @@ public final class DrmInitData implements Comparator, Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeLong(uuid.getMostSignificantBits()); dest.writeLong(uuid.getLeastSignificantBits()); + dest.writeString(licenseServerUrl); dest.writeString(mimeType); dest.writeByteArray(data); dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java index 2699559c5f..78994a9b80 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java @@ -105,101 +105,63 @@ public interface ExoMediaDrm { boolean hasNewUsableKey); } - /** - * @see android.media.MediaDrm.KeyStatus - */ - interface KeyStatus { - /** Returns the status code for the key. */ - int getStatusCode(); - /** Returns the id for the key. */ - byte[] getKeyId(); - } - - /** - * Default implementation of {@link KeyStatus}. - */ - final class DefaultKeyStatus implements KeyStatus { + /** @see android.media.MediaDrm.KeyStatus */ + final class KeyStatus { private final int statusCode; private final byte[] keyId; - DefaultKeyStatus(int statusCode, byte[] keyId) { + public KeyStatus(int statusCode, byte[] keyId) { this.statusCode = statusCode; this.keyId = keyId; } - @Override public int getStatusCode() { return statusCode; } - @Override public byte[] getKeyId() { return keyId; } } - /** - * @see android.media.MediaDrm.KeyRequest - */ - interface KeyRequest { - byte[] getData(); - String getDefaultUrl(); - } - - /** - * Default implementation of {@link KeyRequest}. - */ - final class DefaultKeyRequest implements KeyRequest { + /** @see android.media.MediaDrm.KeyRequest */ + final class KeyRequest { private final byte[] data; private final String defaultUrl; - public DefaultKeyRequest(byte[] data, String defaultUrl) { + public KeyRequest(byte[] data, String defaultUrl) { this.data = data; this.defaultUrl = defaultUrl; } - @Override public byte[] getData() { return data; } - @Override public String getDefaultUrl() { return defaultUrl; } } - /** - * @see android.media.MediaDrm.ProvisionRequest - */ - interface ProvisionRequest { - byte[] getData(); - String getDefaultUrl(); - } - - /** - * Default implementation of {@link ProvisionRequest}. - */ - final class DefaultProvisionRequest implements ProvisionRequest { + /** @see android.media.MediaDrm.ProvisionRequest */ + final class ProvisionRequest { private final byte[] data; private final String defaultUrl; - public DefaultProvisionRequest(byte[] data, String defaultUrl) { + public ProvisionRequest(byte[] data, String defaultUrl) { this.data = data; this.defaultUrl = defaultUrl; } - @Override public byte[] getData() { return data; } - @Override public String getDefaultUrl() { return defaultUrl; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index f960cd637f..8937768ff4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.drm; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.media.DeniedByServerException; import android.media.MediaCrypto; @@ -26,7 +27,9 @@ import android.media.UnsupportedSchemeException; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.HashMap; @@ -40,6 +43,8 @@ import java.util.UUID; @TargetApi(23) public final class FrameworkMediaDrm implements ExoMediaDrm { + private static final String CENC_SCHEME_MIME_TYPE = "cenc"; + private final UUID uuid; private final MediaDrm mediaDrm; @@ -67,6 +72,9 @@ public final class FrameworkMediaDrm implements ExoMediaDrm keyInfo, boolean hasNewUsableKey) { - List exoKeyInfo = new ArrayList<>(); - for (MediaDrm.KeyStatus keyStatus : keyInfo) { - exoKeyInfo.add(new DefaultKeyStatus(keyStatus.getStatusCode(), keyStatus.getKeyId())); - } - listener.onKeyStatusChange(FrameworkMediaDrm.this, sessionId, exoKeyInfo, - hasNewUsableKey); - } - }, null); + + mediaDrm.setOnKeyStatusChangeListener( + listener == null + ? null + : new MediaDrm.OnKeyStatusChangeListener() { + @Override + public void onKeyStatusChange( + @NonNull MediaDrm md, + @NonNull byte[] sessionId, + @NonNull List keyInfo, + boolean hasNewUsableKey) { + List exoKeyInfo = new ArrayList<>(); + for (MediaDrm.KeyStatus keyStatus : keyInfo) { + exoKeyInfo.add(new KeyStatus(keyStatus.getStatusCode(), keyStatus.getKeyId())); + } + listener.onKeyStatusChange( + FrameworkMediaDrm.this, sessionId, exoKeyInfo, hasNewUsableKey); + } + }, + null); } @Override @@ -114,23 +128,63 @@ public final class FrameworkMediaDrm implements ExoMediaDrm optionalParameters) throws NotProvisionedException { + public KeyRequest getKeyRequest( + byte[] scope, + byte[] init, + String mimeType, + int keyType, + HashMap optionalParameters) + throws NotProvisionedException { + + // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. Some Amazon + // devices also required data to be extracted from the PSSH atom for PlayReady. + if ((Util.SDK_INT < 21 && C.WIDEVINE_UUID.equals(uuid)) + || (C.PLAYREADY_UUID.equals(uuid) + && "Amazon".equals(Util.MANUFACTURER) + && ("AFTB".equals(Util.MODEL) // Fire TV Gen 1 + || "AFTS".equals(Util.MODEL) // Fire TV Gen 2 + || "AFTM".equals(Util.MODEL)))) { // Fire TV Stick Gen 1 + byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(init, uuid); + if (psshData == null) { + // Extraction failed. schemeData isn't a PSSH atom, so leave it unchanged. + } else { + init = psshData; + } + } + + // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4. + if (Util.SDK_INT < 26 + && C.CLEARKEY_UUID.equals(uuid) + && (MimeTypes.VIDEO_MP4.equals(mimeType) || MimeTypes.AUDIO_MP4.equals(mimeType))) { + mimeType = CENC_SCHEME_MIME_TYPE; + } + final MediaDrm.KeyRequest request = mediaDrm.getKeyRequest(scope, init, mimeType, keyType, optionalParameters); - return new DefaultKeyRequest(request.getData(), request.getDefaultUrl()); + + byte[] requestData = request.getData(); + if (C.CLEARKEY_UUID.equals(uuid)) { + requestData = ClearKeyUtil.adjustRequestData(requestData); + } + + return new KeyRequest(requestData, request.getDefaultUrl()); } @Override public byte[] provideKeyResponse(byte[] scope, byte[] response) throws NotProvisionedException, DeniedByServerException { + + if (C.CLEARKEY_UUID.equals(uuid)) { + response = ClearKeyUtil.adjustResponseData(response); + } + return mediaDrm.provideKeyResponse(scope, response); } @Override public ProvisionRequest getProvisionRequest() { final MediaDrm.ProvisionRequest request = mediaDrm.getProvisionRequest(); - return new DefaultProvisionRequest(request.getData(), request.getDefaultUrl()); + return new ProvisionRequest(request.getData(), request.getDefaultUrl()); } @Override @@ -183,4 +237,17 @@ public final class FrameworkMediaDrm implements ExoMediaDrmSee GitHub issue #4413. + */ + private static boolean needsForceWidevineL3Workaround() { + return "ASUS_Z00AD".equals(Util.MODEL); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index 4a93ac8333..fc1e62a89c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.drm; import android.annotation.TargetApi; import android.net.Uri; +import android.support.annotation.Nullable; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; @@ -108,13 +109,19 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { @Override public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { - String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); + String url = + request.getDefaultUrl() + "&signedRequest=" + Util.fromUtf8Bytes(request.getData()); return executePost(dataSourceFactory, url, new byte[0], null); } @Override - public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { + public byte[] executeKeyRequest( + UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl) + throws Exception { String url = request.getDefaultUrl(); + if (TextUtils.isEmpty(url)) { + url = mediaProvidedLicenseServerUrl; + } if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) { url = defaultLicenseUrl; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java index 7b9aeca30a..7ed4a61a60 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.drm; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.util.Assertions; @@ -44,7 +45,9 @@ public final class LocalMediaDrmCallback implements MediaDrmCallback { } @Override - public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { + public byte[] executeKeyRequest( + UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl) + throws Exception { return keyResponse; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java index 617e168f9a..4405d6e538 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.drm; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import java.util.UUID; @@ -38,10 +39,13 @@ public interface MediaDrmCallback { * Executes a key request. * * @param uuid The UUID of the content protection scheme. - * @param request The request. + * @param request The request generated by the content decryption module. + * @param mediaProvidedLicenseServerUrl A license server URL provided by the media, or null if the + * media does not include any license server URL. * @return The response data. * @throws Exception If an error occurred executing the request. */ - byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception; - + byte[] executeKeyRequest( + UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl) + throws Exception; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java new file mode 100644 index 0000000000..435fb13648 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java @@ -0,0 +1,573 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor; + +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; + +/** + * A seeker that supports seeking within a stream by searching for the target frame using binary + * search. + * + *

    This seeker operates on a stream that contains multiple frames (or samples). Each frame is + * associated with some kind of timestamps, such as stream time, or frame indices. Given a target + * seek time, the seeker will find the corresponding target timestamp, and perform a search + * operation within the stream to identify the target frame and return the byte position in the + * stream of the target frame. + */ +public abstract class BinarySearchSeeker { + + /** A seeker that looks for a given timestamp from an input. */ + protected interface TimestampSeeker { + + /** + * Searches for a given timestamp from the input. + * + *

    Given a target timestamp and an input stream, this seeker will try to read up to a range + * of {@code searchRangeBytes} bytes from that input, look for all available timestamps from all + * frames in that range, compare those with the target timestamp, and return one of the {@link + * TimestampSearchResult}. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @param targetTimestamp The target timestamp that we are looking for. + * @param outputFrameHolder If {@link TimestampSearchResult#RESULT_TARGET_TIMESTAMP_FOUND} is + * returned, this holder may be updated to hold the extracted frame that contains the target + * frame/sample associated with the target timestamp. + * @return A {@link TimestampSearchResult}, that includes a {@link TimestampSearchResult#result} + * value, and other necessary info: + *

      + *
    • {@link TimestampSearchResult#RESULT_NO_TIMESTAMP} is returned if there is no + * timestamp in the reading range. + *
    • {@link TimestampSearchResult#RESULT_POSITION_UNDERESTIMATED} is returned if all + * timestamps in the range are smaller than the target timestamp. + *
    • {@link TimestampSearchResult#RESULT_POSITION_OVERESTIMATED} is returned if all + * timestamps in the range are larger than the target timestamp. + *
    • {@link TimestampSearchResult#RESULT_TARGET_TIMESTAMP_FOUND} is returned if this + * seeker can find a timestamp that it deems close enough to the given target. + *
    + * + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + TimestampSearchResult searchForTimestamp( + ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder) + throws IOException, InterruptedException; + } + + /** + * Holds a frame extracted from a stream, together with the time stamp of the frame in + * microseconds. + */ + public static final class OutputFrameHolder { + + public long timeUs; + public ByteBuffer byteBuffer; + + /** Constructs an instance, wrapping the given byte buffer. */ + public OutputFrameHolder(ByteBuffer outputByteBuffer) { + this.timeUs = 0; + this.byteBuffer = outputByteBuffer; + } + } + + /** + * A {@link SeekTimestampConverter} implementation that returns the seek time itself as the + * timestamp for a seek time position. + */ + public static final class DefaultSeekTimestampConverter implements SeekTimestampConverter { + + @Override + public long timeUsToTargetTime(long timeUs) { + return timeUs; + } + } + + /** + * A converter that converts seek time in stream time into target timestamp for the {@link + * BinarySearchSeeker}. + */ + protected interface SeekTimestampConverter { + /** + * Converts a seek time in microseconds into target timestamp for the {@link + * BinarySearchSeeker}. + */ + long timeUsToTargetTime(long timeUs); + } + + /** + * When seeking within the source, if the offset is smaller than or equal to this value, the seek + * operation will be performed using a skip operation. Otherwise, the source will be reloaded at + * the new seek position. + */ + private static final long MAX_SKIP_BYTES = 256 * 1024; + + protected final BinarySearchSeekMap seekMap; + protected final TimestampSeeker timestampSeeker; + protected @Nullable SeekOperationParams seekOperationParams; + + private final int minimumSearchRange; + + /** + * Constructs an instance. + * + * @param seekTimestampConverter The {@link SeekTimestampConverter} that converts seek time in + * stream time into target timestamp. + * @param timestampSeeker A {@link TimestampSeeker} that will be used to search for timestamps + * within the stream. + * @param durationUs The duration of the stream in microseconds. + * @param floorTimePosition The minimum timestamp value (inclusive) in the stream. + * @param ceilingTimePosition The minimum timestamp value (exclusive) in the stream. + * @param floorBytePosition The starting position of the frame with minimum timestamp value + * (inclusive) in the stream. + * @param ceilingBytePosition The position after the frame with maximum timestamp value in the + * stream. + * @param approxBytesPerFrame Approximated bytes per frame. + * @param minimumSearchRange The minimum byte range that this binary seeker will operate on. If + * the remaining search range is smaller than this value, the search will stop, and the seeker + * will return the position at the floor of the range as the result. + */ + @SuppressWarnings("initialization") + protected BinarySearchSeeker( + SeekTimestampConverter seekTimestampConverter, + TimestampSeeker timestampSeeker, + long durationUs, + long floorTimePosition, + long ceilingTimePosition, + long floorBytePosition, + long ceilingBytePosition, + long approxBytesPerFrame, + int minimumSearchRange) { + this.timestampSeeker = timestampSeeker; + this.minimumSearchRange = minimumSearchRange; + this.seekMap = + new BinarySearchSeekMap( + seekTimestampConverter, + durationUs, + floorTimePosition, + ceilingTimePosition, + floorBytePosition, + ceilingBytePosition, + approxBytesPerFrame); + } + + /** Returns the seek map for the stream. */ + public final SeekMap getSeekMap() { + return seekMap; + } + + /** + * Sets the target time in microseconds within the stream to seek to. + * + * @param timeUs The target time in microseconds within the stream. + */ + public final void setSeekTargetUs(long timeUs) { + if (seekOperationParams != null && seekOperationParams.getSeekTimeUs() == timeUs) { + return; + } + seekOperationParams = createSeekParamsForTargetTimeUs(timeUs); + } + + /** Returns whether the last operation set by {@link #setSeekTargetUs(long)} is still pending. */ + public final boolean isSeeking() { + return seekOperationParams != null; + } + + /** + * Continues to handle the pending seek operation. Returns one of the {@code RESULT_} values from + * {@link Extractor}. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated + * to hold the position of the required seek. + * @param outputFrameHolder If {@link Extractor#RESULT_CONTINUE} is returned, this holder may be + * updated to hold the extracted frame that contains the target sample. The caller needs to + * check the byte buffer limit to see if an extracted frame is available. + * @return One of the {@code RESULT_} values defined in {@link Extractor}. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + public int handlePendingSeek( + ExtractorInput input, PositionHolder seekPositionHolder, OutputFrameHolder outputFrameHolder) + throws InterruptedException, IOException { + TimestampSeeker timestampSeeker = Assertions.checkNotNull(this.timestampSeeker); + while (true) { + SeekOperationParams seekOperationParams = Assertions.checkNotNull(this.seekOperationParams); + long floorPosition = seekOperationParams.getFloorBytePosition(); + long ceilingPosition = seekOperationParams.getCeilingBytePosition(); + long searchPosition = seekOperationParams.getNextSearchBytePosition(); + + if (ceilingPosition - floorPosition <= minimumSearchRange) { + // The seeking range is too small, so we can just continue from the floor position. + markSeekOperationFinished(/* foundTargetFrame= */ false, floorPosition); + return seekToPosition(input, floorPosition, seekPositionHolder); + } + if (!skipInputUntilPosition(input, searchPosition)) { + return seekToPosition(input, searchPosition, seekPositionHolder); + } + + input.resetPeekPosition(); + TimestampSearchResult timestampSearchResult = + timestampSeeker.searchForTimestamp( + input, seekOperationParams.getTargetTimePosition(), outputFrameHolder); + + switch (timestampSearchResult.result) { + case TimestampSearchResult.RESULT_POSITION_OVERESTIMATED: + seekOperationParams.updateSeekCeiling( + timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate); + break; + case TimestampSearchResult.RESULT_POSITION_UNDERESTIMATED: + seekOperationParams.updateSeekFloor( + timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate); + break; + case TimestampSearchResult.RESULT_TARGET_TIMESTAMP_FOUND: + markSeekOperationFinished( + /* foundTargetFrame= */ true, timestampSearchResult.bytePositionToUpdate); + skipInputUntilPosition(input, timestampSearchResult.bytePositionToUpdate); + return seekToPosition( + input, timestampSearchResult.bytePositionToUpdate, seekPositionHolder); + case TimestampSearchResult.RESULT_NO_TIMESTAMP: + // We can't find any timestamp in the search range from the search position. + // Give up, and just continue reading from the last search position in this case. + markSeekOperationFinished(/* foundTargetFrame= */ false, searchPosition); + return seekToPosition(input, searchPosition, seekPositionHolder); + default: + throw new IllegalStateException("Invalid case"); + } + } + } + + protected SeekOperationParams createSeekParamsForTargetTimeUs(long timeUs) { + return new SeekOperationParams( + timeUs, + seekMap.timeUsToTargetTime(timeUs), + seekMap.floorTimePosition, + seekMap.ceilingTimePosition, + seekMap.floorBytePosition, + seekMap.ceilingBytePosition, + seekMap.approxBytesPerFrame); + } + + protected final void markSeekOperationFinished(boolean foundTargetFrame, long resultPosition) { + seekOperationParams = null; + onSeekOperationFinished(foundTargetFrame, resultPosition); + } + + protected void onSeekOperationFinished(boolean foundTargetFrame, long resultPosition) { + // Do nothing. + } + + protected final boolean skipInputUntilPosition(ExtractorInput input, long position) + throws IOException, InterruptedException { + long bytesToSkip = position - input.getPosition(); + if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) { + input.skipFully((int) bytesToSkip); + return true; + } + return false; + } + + protected final int seekToPosition( + ExtractorInput input, long position, PositionHolder seekPositionHolder) { + if (position == input.getPosition()) { + return Extractor.RESULT_CONTINUE; + } else { + seekPositionHolder.position = position; + return Extractor.RESULT_SEEK; + } + } + + /** + * Contains parameters for a pending seek operation by {@link BinarySearchSeeker}. + * + *

    This class holds parameters for a binary-search for the {@code targetTimePosition} in the + * range [floorPosition, ceilingPosition). + */ + protected static class SeekOperationParams { + private final long seekTimeUs; + private final long targetTimePosition; + private final long approxBytesPerFrame; + + private long floorTimePosition; + private long ceilingTimePosition; + private long floorBytePosition; + private long ceilingBytePosition; + private long nextSearchBytePosition; + + /** + * Returns the next position in the stream to search for target frame, given [floorBytePosition, + * ceilingBytePosition), with corresponding [floorTimePosition, ceilingTimePosition). + */ + protected static long calculateNextSearchBytePosition( + long targetTimePosition, + long floorTimePosition, + long ceilingTimePosition, + long floorBytePosition, + long ceilingBytePosition, + long approxBytesPerFrame) { + if (floorBytePosition + 1 >= ceilingBytePosition + || floorTimePosition + 1 >= ceilingTimePosition) { + return floorBytePosition; + } + long seekTimeDuration = targetTimePosition - floorTimePosition; + float estimatedBytesPerTimeUnit = + (float) (ceilingBytePosition - floorBytePosition) + / (ceilingTimePosition - floorTimePosition); + // It's better to under-estimate rather than over-estimate, because the extractor + // input can skip forward easily, but cannot rewind easily (it may require a new connection + // to be made). + // Therefore, we should reduce the estimated position by some amount, so it will converge to + // the correct frame earlier. + long bytesToSkip = (long) (seekTimeDuration * estimatedBytesPerTimeUnit); + long confidenceInterval = bytesToSkip / 20; + long estimatedFramePosition = floorBytePosition + bytesToSkip - approxBytesPerFrame; + long estimatedPosition = estimatedFramePosition - confidenceInterval; + return Util.constrainValue(estimatedPosition, floorBytePosition, ceilingBytePosition - 1); + } + + protected SeekOperationParams( + long seekTimeUs, + long targetTimePosition, + long floorTimePosition, + long ceilingTimePosition, + long floorBytePosition, + long ceilingBytePosition, + long approxBytesPerFrame) { + this.seekTimeUs = seekTimeUs; + this.targetTimePosition = targetTimePosition; + this.floorTimePosition = floorTimePosition; + this.ceilingTimePosition = ceilingTimePosition; + this.floorBytePosition = floorBytePosition; + this.ceilingBytePosition = ceilingBytePosition; + this.approxBytesPerFrame = approxBytesPerFrame; + this.nextSearchBytePosition = + calculateNextSearchBytePosition( + targetTimePosition, + floorTimePosition, + ceilingTimePosition, + floorBytePosition, + ceilingBytePosition, + approxBytesPerFrame); + } + + /** + * Returns the floor byte position of the range [floorPosition, ceilingPosition) for this seek + * operation. + */ + private long getFloorBytePosition() { + return floorBytePosition; + } + + /** + * Returns the ceiling byte position of the range [floorPosition, ceilingPosition) for this seek + * operation. + */ + private long getCeilingBytePosition() { + return ceilingBytePosition; + } + + /** Returns the target timestamp as translated from the seek time. */ + private long getTargetTimePosition() { + return targetTimePosition; + } + + /** Returns the target seek time in microseconds. */ + private long getSeekTimeUs() { + return seekTimeUs; + } + + /** Updates the floor constraints (inclusive) of the seek operation. */ + private void updateSeekFloor(long floorTimePosition, long floorBytePosition) { + this.floorTimePosition = floorTimePosition; + this.floorBytePosition = floorBytePosition; + updateNextSearchBytePosition(); + } + + /** Updates the ceiling constraints (exclusive) of the seek operation. */ + private void updateSeekCeiling(long ceilingTimePosition, long ceilingBytePosition) { + this.ceilingTimePosition = ceilingTimePosition; + this.ceilingBytePosition = ceilingBytePosition; + updateNextSearchBytePosition(); + } + + /** Returns the next position in the stream to search. */ + private long getNextSearchBytePosition() { + return nextSearchBytePosition; + } + + private void updateNextSearchBytePosition() { + this.nextSearchBytePosition = + calculateNextSearchBytePosition( + targetTimePosition, + floorTimePosition, + ceilingTimePosition, + floorBytePosition, + ceilingBytePosition, + approxBytesPerFrame); + } + } + + /** + * Represents possible search results for {@link + * TimestampSeeker#searchForTimestamp(ExtractorInput, long, OutputFrameHolder)}. + */ + public static final class TimestampSearchResult { + + public static final int RESULT_TARGET_TIMESTAMP_FOUND = 0; + public static final int RESULT_POSITION_OVERESTIMATED = -1; + public static final int RESULT_POSITION_UNDERESTIMATED = -2; + public static final int RESULT_NO_TIMESTAMP = -3; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + RESULT_TARGET_TIMESTAMP_FOUND, + RESULT_POSITION_OVERESTIMATED, + RESULT_POSITION_UNDERESTIMATED, + RESULT_NO_TIMESTAMP + }) + @interface SearchResult {} + + public static final TimestampSearchResult NO_TIMESTAMP_IN_RANGE_RESULT = + new TimestampSearchResult(RESULT_NO_TIMESTAMP, C.TIME_UNSET, C.POSITION_UNSET); + + /** @see TimestampSeeker */ + private final @SearchResult int result; + + /** + * When {@code result} is {@link #RESULT_POSITION_OVERESTIMATED}, the {@link + * SeekOperationParams#ceilingTimePosition} should be updated with this value. When {@code + * result} is {@link #RESULT_POSITION_UNDERESTIMATED}, the {@link + * SeekOperationParams#floorTimePosition} should be updated with this value. + */ + private final long timestampToUpdate; + /** + * When {@code result} is {@link #RESULT_POSITION_OVERESTIMATED}, the {@link + * SeekOperationParams#ceilingBytePosition} should be updated with this value. When {@code + * result} is {@link #RESULT_POSITION_UNDERESTIMATED}, the {@link + * SeekOperationParams#floorBytePosition} should be updated with this value. + */ + private final long bytePositionToUpdate; + + private TimestampSearchResult( + @SearchResult int result, long timestampToUpdate, long bytePositionToUpdate) { + this.result = result; + this.timestampToUpdate = timestampToUpdate; + this.bytePositionToUpdate = bytePositionToUpdate; + } + + /** + * Returns a result to signal that the current position in the input stream overestimates the + * true position of the target frame, and the {@link BinarySearchSeeker} should modify its + * {@link SeekOperationParams}'s ceiling timestamp and byte position using the given values. + */ + public static TimestampSearchResult overestimatedResult( + long newCeilingTimestamp, long newCeilingBytePosition) { + return new TimestampSearchResult( + RESULT_POSITION_OVERESTIMATED, newCeilingTimestamp, newCeilingBytePosition); + } + + /** + * Returns a result to signal that the current position in the input stream underestimates the + * true position of the target frame, and the {@link BinarySearchSeeker} should modify its + * {@link SeekOperationParams}'s floor timestamp and byte position using the given values. + */ + public static TimestampSearchResult underestimatedResult( + long newFloorTimestamp, long newCeilingBytePosition) { + return new TimestampSearchResult( + RESULT_POSITION_UNDERESTIMATED, newFloorTimestamp, newCeilingBytePosition); + } + + /** + * Returns a result to signal that the target timestamp has been found at the {@code + * resultBytePosition}, and the seek operation can stop. + * + *

    Note that when this value is returned from {@link + * TimestampSeeker#searchForTimestamp(ExtractorInput, long, OutputFrameHolder)}, the {@link + * OutputFrameHolder} may be updated to hold the target frame as an optimization. + */ + public static TimestampSearchResult targetFoundResult(long resultBytePosition) { + return new TimestampSearchResult( + RESULT_TARGET_TIMESTAMP_FOUND, C.TIME_UNSET, resultBytePosition); + } + } + + /** + * A {@link SeekMap} implementation that returns the estimated byte location from {@link + * SeekOperationParams#calculateNextSearchBytePosition(long, long, long, long, long, long)} for + * each {@link #getSeekPoints(long)} query. + */ + public static class BinarySearchSeekMap implements SeekMap { + private final SeekTimestampConverter seekTimestampConverter; + private final long durationUs; + private final long floorTimePosition; + private final long ceilingTimePosition; + private final long floorBytePosition; + private final long ceilingBytePosition; + private final long approxBytesPerFrame; + + /** Constructs a new instance of this seek map. */ + public BinarySearchSeekMap( + SeekTimestampConverter seekTimestampConverter, + long durationUs, + long floorTimePosition, + long ceilingTimePosition, + long floorBytePosition, + long ceilingBytePosition, + long approxBytesPerFrame) { + this.seekTimestampConverter = seekTimestampConverter; + this.durationUs = durationUs; + this.floorTimePosition = floorTimePosition; + this.ceilingTimePosition = ceilingTimePosition; + this.floorBytePosition = floorBytePosition; + this.ceilingBytePosition = ceilingBytePosition; + this.approxBytesPerFrame = approxBytesPerFrame; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + long nextSearchPosition = + SeekOperationParams.calculateNextSearchBytePosition( + /* targetTimePosition= */ seekTimestampConverter.timeUsToTargetTime(timeUs), + /* floorTimePosition= */ floorTimePosition, + /* ceilingTimePosition= */ ceilingTimePosition, + /* floorBytePosition= */ floorBytePosition, + /* ceilingBytePosition= */ ceilingBytePosition, + /* approxBytesPerFrame= */ approxBytesPerFrame); + return new SeekPoints(new SeekPoint(timeUs, nextSearchPosition)); + } + + @Override + public long getDurationUs() { + return durationUs; + } + + /** @see SeekTimestampConverter#timeUsToTargetTime(long) */ + public long timeUsToTargetTime(long timeUs) { + return seekTimestampConverter.timeUsToTargetTime(timeUs); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java new file mode 100644 index 0000000000..abce01b5ef --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Util; + +/** + * A {@link SeekMap} implementation that assumes the stream has a constant bitrate and consists of + * multiple independent frames of the same size. Seek points are calculated to be at frame + * boundaries. + */ +public class ConstantBitrateSeekMap implements SeekMap { + + private final long inputLength; + private final long firstFrameBytePosition; + private final int frameSize; + private final long dataSize; + private final int bitrate; + private final long durationUs; + + /** + * Constructs a new instance from a stream. + * + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param firstFrameBytePosition The byte-position of the first frame in the stream. + * @param bitrate The bitrate (which is assumed to be constant in the stream). + * @param frameSize The size of each frame in the stream in bytes. May be {@link C#LENGTH_UNSET} + * if unknown. + */ + public ConstantBitrateSeekMap( + long inputLength, long firstFrameBytePosition, int bitrate, int frameSize) { + this.inputLength = inputLength; + this.firstFrameBytePosition = firstFrameBytePosition; + this.frameSize = frameSize == C.LENGTH_UNSET ? 1 : frameSize; + this.bitrate = bitrate; + + if (inputLength == C.LENGTH_UNSET) { + dataSize = C.LENGTH_UNSET; + durationUs = C.TIME_UNSET; + } else { + dataSize = inputLength - firstFrameBytePosition; + durationUs = getTimeUsAtPosition(inputLength, firstFrameBytePosition, bitrate); + } + } + + @Override + public boolean isSeekable() { + return dataSize != C.LENGTH_UNSET; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + if (dataSize == C.LENGTH_UNSET) { + return new SeekPoints(new SeekPoint(0, firstFrameBytePosition)); + } + long seekFramePosition = getFramePositionForTimeUs(timeUs); + long seekTimeUs = getTimeUsAtPosition(seekFramePosition); + SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekFramePosition); + if (seekTimeUs >= timeUs || seekFramePosition + frameSize >= inputLength) { + return new SeekPoints(seekPoint); + } else { + long secondSeekPosition = seekFramePosition + frameSize; + long secondSeekTimeUs = getTimeUsAtPosition(secondSeekPosition); + SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); + return new SeekPoints(seekPoint, secondSeekPoint); + } + } + + @Override + public long getDurationUs() { + return durationUs; + } + + /** + * Returns the stream time in microseconds for a given position. + * + * @param position The stream byte-position. + * @return The stream time in microseconds for the given position. + */ + public long getTimeUsAtPosition(long position) { + return getTimeUsAtPosition(position, firstFrameBytePosition, bitrate); + } + + // Internal methods + + /** + * Returns the stream time in microseconds for a given stream position. + * + * @param position The stream byte-position. + * @param firstFrameBytePosition The position of the first frame in the stream. + * @param bitrate The bitrate (which is assumed to be constant in the stream). + * @return The stream time in microseconds for the given stream position. + */ + private static long getTimeUsAtPosition(long position, long firstFrameBytePosition, int bitrate) { + return Math.max(0, position - firstFrameBytePosition) + * C.BITS_PER_BYTE + * C.MICROS_PER_SECOND + / bitrate; + } + + private long getFramePositionForTimeUs(long timeUs) { + long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * C.BITS_PER_BYTE); + // Constrain to nearest preceding frame offset. + positionOffset = (positionOffset / frameSize) * frameSize; + positionOffset = + Util.constrainValue(positionOffset, /* min= */ 0, /* max= */ dataSize - frameSize); + return firstFrameBytePosition + positionOffset; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index 425f2b77cd..f2c3d982d2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -72,6 +72,9 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { FLAC_EXTRACTOR_CONSTRUCTOR = flacExtractorConstructor; } + private boolean constantBitrateSeekingEnabled; + private @AdtsExtractor.Flags int adtsFlags; + private @AmrExtractor.Flags int amrFlags; private @MatroskaExtractor.Flags int matroskaFlags; private @Mp4Extractor.Flags int mp4Flags; private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags; @@ -83,6 +86,45 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { tsMode = TsExtractor.MODE_SINGLE_PMT; } + /** + * Convenience method to set whether approximate seeking using constant bitrate assumptions should + * be enabled for all extractors that support it. If set to true, the flags required to enable + * this functionality will be OR'd with those passed to the setters when creating extractor + * instances. If set to false then the flags passed to the setters will be used without + * modification. + * + * @param constantBitrateSeekingEnabled Whether approximate seeking using a constant bitrate + * assumption should be enabled for all extractors that support it. + */ + public void setConstantBitrateSeekingEnabled(boolean constantBitrateSeekingEnabled) { + this.constantBitrateSeekingEnabled = constantBitrateSeekingEnabled; + } + + /** + * Sets flags for {@link AdtsExtractor} instances created by the factory. + * + * @see AdtsExtractor#AdtsExtractor(long, int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setAdtsExtractorFlags( + @AdtsExtractor.Flags int flags) { + this.adtsFlags = flags; + return this; + } + + /** + * Sets flags for {@link AmrExtractor} instances created by the factory. + * + * @see AmrExtractor#AmrExtractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setAmrExtractorFlags(@AmrExtractor.Flags int flags) { + this.amrFlags = flags; + return this; + } + /** * Sets flags for {@link MatroskaExtractor} instances created by the factory. * @@ -165,15 +207,31 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { extractors[0] = new MatroskaExtractor(matroskaFlags); extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags); extractors[2] = new Mp4Extractor(mp4Flags); - extractors[3] = new Mp3Extractor(mp3Flags); - extractors[4] = new AdtsExtractor(); + extractors[3] = + new Mp3Extractor( + mp3Flags + | (constantBitrateSeekingEnabled + ? Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0)); + extractors[4] = + new AdtsExtractor( + /* firstStreamSampleTimestampUs= */ 0, + adtsFlags + | (constantBitrateSeekingEnabled + ? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0)); extractors[5] = new Ac3Extractor(); extractors[6] = new TsExtractor(tsMode, tsFlags); extractors[7] = new FlvExtractor(); extractors[8] = new OggExtractor(); extractors[9] = new PsExtractor(); extractors[10] = new WavExtractor(); - extractors[11] = new AmrExtractor(); + extractors[11] = + new AmrExtractor( + amrFlags + | (constantBitrateSeekingEnabled + ? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0)); if (FLAC_EXTRACTOR_CONSTRUCTOR != null) { try { extractors[12] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java index c023b0de95..9eaf0f7ef7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -50,9 +51,12 @@ public final class DummyTrackOutput implements TrackOutput { } @Override - public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - CryptoData cryptoData) { + public void sampleMetadata( + long timeUs, + @C.BufferFlags int flags, + int size, + int offset, + @Nullable CryptoData cryptoData) { // Do nothing. } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java index 7a2bc15da9..c63aad541b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java @@ -15,8 +15,11 @@ */ package com.google.android.exoplayer2.extractor; +import android.support.annotation.IntDef; import com.google.android.exoplayer2.C; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Extracts media data from a container format. @@ -41,6 +44,11 @@ public interface Extractor { */ int RESULT_END_OF_INPUT = C.RESULT_END_OF_INPUT; + /** Result values that can be returned by {@link #read(ExtractorInput, PositionHolder)}. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = {RESULT_CONTINUE, RESULT_SEEK, RESULT_END_OF_INPUT}) + @interface ReadResult {} + /** * Returns whether this extractor can extract samples from the {@link ExtractorInput}, which must * provide data from the start of the stream. @@ -63,14 +71,14 @@ public interface Extractor { void init(ExtractorOutput output); /** - * Extracts data read from a provided {@link ExtractorInput}. Must not be called before - * {@link #init(ExtractorOutput)}. - *

    - * A single call to this method will block until some progress has been made, but will not block - * for longer than this. Hence each call will consume only a small amount of input data. - *

    - * In the common case, {@link #RESULT_CONTINUE} is returned to indicate that the - * {@link ExtractorInput} passed to the next read is required to provide data continuing from the + * Extracts data read from a provided {@link ExtractorInput}. Must not be called before {@link + * #init(ExtractorOutput)}. + * + *

    A single call to this method will block until some progress has been made, but will not + * block for longer than this. Hence each call will consume only a small amount of input data. + * + *

    In the common case, {@link #RESULT_CONTINUE} is returned to indicate that the {@link + * ExtractorInput} passed to the next read is required to provide data continuing from the * position in the stream reached by the returning call. If the extractor requires data to be * provided from a different position, then that position is set in {@code seekPosition} and * {@link #RESULT_SEEK} is returned. If the extractor reached the end of the data provided by the @@ -83,6 +91,7 @@ public interface Extractor { * @throws IOException If an error occurred reading from the input. * @throws InterruptedException If the thread was interrupted. */ + @ReadResult int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java index 75d8b4cf2d..54d48350fc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; +import com.google.android.exoplayer2.metadata.id3.InternalFrame; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -39,7 +40,8 @@ public final class GaplessInfoHolder { } }; - private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; + private static final String GAPLESS_DOMAIN = "com.apple.iTunes"; + private static final String GAPLESS_DESCRIPTION = "iTunSMPB"; private static final Pattern GAPLESS_COMMENT_PATTERN = Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); @@ -91,7 +93,15 @@ public final class GaplessInfoHolder { Metadata.Entry entry = metadata.get(i); if (entry instanceof CommentFrame) { CommentFrame commentFrame = (CommentFrame) entry; - if (setFromComment(commentFrame.description, commentFrame.text)) { + if (GAPLESS_DESCRIPTION.equals(commentFrame.description) + && setFromComment(commentFrame.text)) { + return true; + } + } else if (entry instanceof InternalFrame) { + InternalFrame internalFrame = (InternalFrame) entry; + if (GAPLESS_DOMAIN.equals(internalFrame.domain) + && GAPLESS_DESCRIPTION.equals(internalFrame.description) + && setFromComment(internalFrame.text)) { return true; } } @@ -103,14 +113,10 @@ public final class GaplessInfoHolder { * Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header * or MPEG 4 user data), if valid and non-zero. * - * @param name The comment's identifier. * @param data The comment's payload data. * @return Whether the holder was populated. */ - private boolean setFromComment(String name, String data) { - if (!GAPLESS_COMMENT_ID.equals(name)) { - return false; - } + private boolean setFromComment(String data) { Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); if (matcher.find()) { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java index 6a8cef6b64..7b832eb400 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java @@ -125,21 +125,23 @@ public interface TrackOutput { /** * Called when metadata associated with a sample has been extracted from the stream. - *

    - * The corresponding sample data will have already been passed to the output via calls to - * {@link #sampleData(ExtractorInput, int, boolean)} or - * {@link #sampleData(ParsableByteArray, int)}. + * + *

    The corresponding sample data will have already been passed to the output via calls to + * {@link #sampleData(ExtractorInput, int, boolean)} or {@link #sampleData(ParsableByteArray, + * int)}. * * @param timeUs The media timestamp associated with the sample, in microseconds. * @param flags Flags associated with the sample. See {@code C.BUFFER_FLAG_*}. * @param size The size of the sample data, in bytes. - * @param offset The number of bytes that have been passed to - * {@link #sampleData(ExtractorInput, int, boolean)} or - * {@link #sampleData(ParsableByteArray, int)} since the last byte belonging to the sample - * whose metadata is being passed. + * @param offset The number of bytes that have been passed to {@link #sampleData(ExtractorInput, + * int, boolean)} or {@link #sampleData(ParsableByteArray, int)} since the last byte belonging + * to the sample whose metadata is being passed. * @param encryptionData The encryption data required to decrypt the sample. May be null. */ - void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - CryptoData encryptionData); - + void sampleMetadata( + long timeUs, + @C.BufferFlags int flags, + int size, + int offset, + @Nullable CryptoData encryptionData); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java index b58e979c26..b94ea7cb58 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java @@ -15,9 +15,12 @@ */ package com.google.android.exoplayer2.extractor.amr; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -29,6 +32,8 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Arrays; /** @@ -40,14 +45,19 @@ import java.util.Arrays; public final class AmrExtractor implements Extractor { /** Factory for {@link AmrExtractor} instances. */ - public static final ExtractorsFactory FACTORY = - new ExtractorsFactory() { + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AmrExtractor()}; - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new AmrExtractor()}; - } - }; + /** Flags controlling the behavior of the extractor. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}) + public @interface Flags {} + /** + * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would + * otherwise not be possible. + */ + public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; /** * The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR @@ -100,23 +110,43 @@ public final class AmrExtractor implements Extractor { /** Theoretical maximum frame size for a AMR frame. */ private static final int MAX_FRAME_SIZE_BYTES = frameSizeBytesByTypeWb[8]; + /** + * The required number of samples in the stream with same sample size to classify the stream as a + * constant-bitrate-stream. + */ + private static final int NUM_SAME_SIZE_CONSTANT_BIT_RATE_THRESHOLD = 20; private static final int SAMPLE_RATE_WB = 16_000; private static final int SAMPLE_RATE_NB = 8_000; private static final int SAMPLE_TIME_PER_FRAME_US = 20_000; private final byte[] scratch; + private final @Flags int flags; private boolean isWideBand; private long currentSampleTimeUs; - private int currentSampleTotalBytes; + private int currentSampleSize; private int currentSampleBytesRemaining; + private boolean hasOutputSeekMap; + private long firstSamplePosition; + private int firstSampleSize; + private int numSamplesWithSameSize; + private long timeOffsetUs; + private ExtractorOutput extractorOutput; private TrackOutput trackOutput; + private @Nullable SeekMap seekMap; private boolean hasOutputFormat; public AmrExtractor() { + this(/* flags= */ 0); + } + + /** @param flags Flags that control the extractor's behavior. */ + public AmrExtractor(@Flags int flags) { + this.flags = flags; scratch = new byte[1]; + firstSampleSize = C.LENGTH_UNSET; } // Extractor implementation. @@ -127,10 +157,10 @@ public final class AmrExtractor implements Extractor { } @Override - public void init(ExtractorOutput output) { - output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); - trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_AUDIO); - output.endTracks(); + public void init(ExtractorOutput extractorOutput) { + this.extractorOutput = extractorOutput; + trackOutput = extractorOutput.track(/* id= */ 0, C.TRACK_TYPE_AUDIO); + extractorOutput.endTracks(); } @Override @@ -142,14 +172,21 @@ public final class AmrExtractor implements Extractor { } } maybeOutputFormat(); - return readSample(input); + int sampleReadResult = readSample(input); + maybeOutputSeekMap(input.getLength(), sampleReadResult); + return sampleReadResult; } @Override public void seek(long position, long timeUs) { currentSampleTimeUs = 0; - currentSampleTotalBytes = 0; + currentSampleSize = 0; currentSampleBytesRemaining = 0; + if (position != 0 && seekMap instanceof ConstantBitrateSeekMap) { + timeOffsetUs = ((ConstantBitrateSeekMap) seekMap).getTimeUsAtPosition(position); + } else { + timeOffsetUs = 0; + } } @Override @@ -228,11 +265,18 @@ public final class AmrExtractor implements Extractor { private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException { if (currentSampleBytesRemaining == 0) { try { - currentSampleTotalBytes = readNextSampleSize(extractorInput); + currentSampleSize = peekNextSampleSize(extractorInput); } catch (EOFException e) { return RESULT_END_OF_INPUT; } - currentSampleBytesRemaining = currentSampleTotalBytes; + currentSampleBytesRemaining = currentSampleSize; + if (firstSampleSize == C.LENGTH_UNSET) { + firstSamplePosition = extractorInput.getPosition(); + firstSampleSize = currentSampleSize; + } + if (firstSampleSize == currentSampleSize) { + numSamplesWithSameSize++; + } } int bytesAppended = @@ -247,16 +291,16 @@ public final class AmrExtractor implements Extractor { } trackOutput.sampleMetadata( - currentSampleTimeUs, + timeOffsetUs + currentSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, - currentSampleTotalBytes, + currentSampleSize, /* offset= */ 0, /* encryptionData= */ null); currentSampleTimeUs += SAMPLE_TIME_PER_FRAME_US; return RESULT_CONTINUE; } - private int readNextSampleSize(ExtractorInput extractorInput) + private int peekNextSampleSize(ExtractorInput extractorInput) throws IOException, InterruptedException { extractorInput.resetPeekPosition(); extractorInput.peekFully(scratch, /* offset= */ 0, /* length= */ 1); @@ -296,4 +340,39 @@ public final class AmrExtractor implements Extractor { // For narrow band, type 12-14 are for future use. return !isWideBand && (frameType < 12 || frameType > 14); } + + private void maybeOutputSeekMap(long inputLength, int sampleReadResult) { + if (hasOutputSeekMap) { + return; + } + + if ((flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) == 0 + || inputLength == C.LENGTH_UNSET + || (firstSampleSize != C.LENGTH_UNSET && firstSampleSize != currentSampleSize)) { + seekMap = new SeekMap.Unseekable(C.TIME_UNSET); + extractorOutput.seekMap(seekMap); + hasOutputSeekMap = true; + } else if (numSamplesWithSameSize >= NUM_SAME_SIZE_CONSTANT_BIT_RATE_THRESHOLD + || sampleReadResult == RESULT_END_OF_INPUT) { + seekMap = getConstantBitrateSeekMap(inputLength); + extractorOutput.seekMap(seekMap); + hasOutputSeekMap = true; + } + } + + private SeekMap getConstantBitrateSeekMap(long inputLength) { + int bitrate = getBitrateFromFrameSize(firstSampleSize, SAMPLE_TIME_PER_FRAME_US); + return new ConstantBitrateSeekMap(inputLength, firstSamplePosition, bitrate, firstSampleSize); + } + + /** + * Returns the stream bitrate, given a frame size and the duration of that frame in microseconds. + * + * @param frameSize The size of each frame in the stream. + * @param durationUsPerFrame The duration of the given frame in microseconds. + * @return The stream bitrate. + */ + private static int getBitrateFromFrameSize(int frameSize, long durationUsPerFrame) { + return (int) ((frameSize * C.BITS_PER_BYTE * C.MICROS_PER_SECOND) / durationUsPerFrame); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index d908f28945..604a520526 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -34,17 +34,8 @@ import java.lang.annotation.RetentionPolicy; */ public final class FlvExtractor implements Extractor { - /** - * Factory for {@link FlvExtractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { - - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new FlvExtractor()}; - } - - }; + /** Factory for {@link FlvExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlvExtractor()}; /** * Extractor states. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java index 21cb3775e5..c0494e1ee0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java @@ -24,7 +24,7 @@ import java.io.EOFException; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.Stack; +import java.util.ArrayDeque; /** * Default implementation of {@link EbmlReader}. @@ -46,15 +46,21 @@ import java.util.Stack; private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4; private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8; - private final byte[] scratch = new byte[8]; - private final Stack masterElementsStack = new Stack<>(); - private final VarintReader varintReader = new VarintReader(); + private final byte[] scratch; + private final ArrayDeque masterElementsStack; + private final VarintReader varintReader; private EbmlReaderOutput output; private @ElementState int elementState; private int elementId; private long elementContentSize; + public DefaultEbmlReader() { + scratch = new byte[8]; + masterElementsStack = new ArrayDeque<>(); + varintReader = new VarintReader(); + } + @Override public void init(EbmlReaderOutput eventHandler) { this.output = eventHandler; @@ -100,7 +106,7 @@ import java.util.Stack; case EbmlReaderOutput.TYPE_MASTER: long elementContentPosition = input.getPosition(); long elementEndPosition = elementContentPosition + elementContentSize; - masterElementsStack.add(new MasterElement(elementId, elementEndPosition)); + masterElementsStack.push(new MasterElement(elementId, elementEndPosition)); output.startMasterElement(elementId, elementContentPosition, elementContentSize); elementState = ELEMENT_STATE_READ_ID; return true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 1049554f7a..355e299325 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -61,17 +61,8 @@ import java.util.UUID; */ public final class MatroskaExtractor implements Extractor { - /** - * Factory for {@link MatroskaExtractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { - - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new MatroskaExtractor()}; - } - - }; + /** Factory for {@link MatroskaExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new MatroskaExtractor()}; /** * Flags controlling the behavior of the extractor. @@ -616,10 +607,10 @@ public final class MatroskaExtractor implements Extractor { currentTrack.number = (int) value; break; case ID_FLAG_DEFAULT: - currentTrack.flagForced = value == 1; + currentTrack.flagDefault = value == 1; break; case ID_FLAG_FORCED: - currentTrack.flagDefault = value == 1; + currentTrack.flagForced = value == 1; break; case ID_TRACK_TYPE: currentTrack.type = (int) value; @@ -1560,7 +1551,7 @@ public final class MatroskaExtractor implements Extractor { if (!foundSyncframe) { input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH); input.resetPeekPosition(); - if ((Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == C.INDEX_UNSET)) { + if (Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == 0) { return; } foundSyncframe = true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java index a3fde6d455..62c9404916 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java @@ -78,8 +78,9 @@ import java.io.IOException; return false; } if (size != 0) { - input.advancePeekPosition((int) size); - peekLength += size; + int sizeInt = (int) size; + input.advancePeekPosition(sizeInt); + peekLength += sizeInt; } } return peekLength == headerStart + headerSize; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java index d358c0cae1..bffc43a540 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java @@ -16,78 +16,27 @@ package com.google.android.exoplayer2.extractor.mp3; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap; import com.google.android.exoplayer2.extractor.MpegAudioHeader; -import com.google.android.exoplayer2.extractor.SeekPoint; -import com.google.android.exoplayer2.util.Util; /** * MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate. */ -/* package */ final class ConstantBitrateSeeker implements Mp3Extractor.Seeker { - - private static final int BITS_PER_BYTE = 8; - - private final long firstFramePosition; - private final int frameSize; - private final long dataSize; - private final int bitrate; - private final long durationUs; +/* package */ final class ConstantBitrateSeeker extends ConstantBitrateSeekMap + implements Mp3Extractor.Seeker { /** * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. * @param firstFramePosition The position of the first frame in the stream. * @param mpegAudioHeader The MPEG audio header associated with the first frame. */ - public ConstantBitrateSeeker(long inputLength, long firstFramePosition, - MpegAudioHeader mpegAudioHeader) { - this.firstFramePosition = firstFramePosition; - this.frameSize = mpegAudioHeader.frameSize; - this.bitrate = mpegAudioHeader.bitrate; - if (inputLength == C.LENGTH_UNSET) { - dataSize = C.LENGTH_UNSET; - durationUs = C.TIME_UNSET; - } else { - dataSize = inputLength - firstFramePosition; - durationUs = getTimeUs(inputLength); - } - } - - @Override - public boolean isSeekable() { - return dataSize != C.LENGTH_UNSET; - } - - @Override - public SeekPoints getSeekPoints(long timeUs) { - if (dataSize == C.LENGTH_UNSET) { - return new SeekPoints(new SeekPoint(0, firstFramePosition)); - } - long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); - // Constrain to nearest preceding frame offset. - positionOffset = (positionOffset / frameSize) * frameSize; - positionOffset = Util.constrainValue(positionOffset, 0, dataSize - frameSize); - long seekPosition = firstFramePosition + positionOffset; - long seekTimeUs = getTimeUs(seekPosition); - SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); - if (seekTimeUs >= timeUs || positionOffset == dataSize - frameSize) { - return new SeekPoints(seekPoint); - } else { - long secondSeekPosition = seekPosition + frameSize; - long secondSeekTimeUs = getTimeUs(secondSeekPosition); - SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); - return new SeekPoints(seekPoint, secondSeekPoint); - } + public ConstantBitrateSeeker( + long inputLength, long firstFramePosition, MpegAudioHeader mpegAudioHeader) { + super(inputLength, firstFramePosition, mpegAudioHeader.bitrate, mpegAudioHeader.frameSize); } @Override public long getTimeUs(long position) { - return (Math.max(0, position - firstFramePosition) * C.MICROS_PER_SECOND * BITS_PER_BYTE) - / bitrate; + return getTimeUsAtPosition(position); } - - @Override - public long getDurationUs() { - return durationUs; - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index bd786191a0..73dd0ec218 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -43,17 +43,8 @@ import java.lang.annotation.RetentionPolicy; */ public final class Mp3Extractor implements Extractor { - /** - * Factory for {@link Mp3Extractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { - - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new Mp3Extractor()}; - } - - }; + /** Factory for {@link Mp3Extractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Mp3Extractor()}; /** * Flags controlling the behavior of the extractor. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 21d861af30..f59214fc37 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mp4; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; @@ -214,14 +215,14 @@ import java.util.List; /** * Returns the child leaf of the given type. - *

    - * If no child exists with the given type then null is returned. If multiple children exist with - * the given type then the first one to have been added is returned. + * + *

    If no child exists with the given type then null is returned. If multiple children exist + * with the given type then the first one to have been added is returned. * * @param type The leaf type. * @return The child leaf of the given type, or null if no such child exists. */ - public LeafAtom getLeafAtomOfType(int type) { + public @Nullable LeafAtom getLeafAtomOfType(int type) { int childrenSize = leafChildren.size(); for (int i = 0; i < childrenSize; i++) { LeafAtom atom = leafChildren.get(i); @@ -234,14 +235,14 @@ import java.util.List; /** * Returns the child container of the given type. - *

    - * If no child exists with the given type then null is returned. If multiple children exist with - * the given type then the first one to have been added is returned. + * + *

    If no child exists with the given type then null is returned. If multiple children exist + * with the given type then the first one to have been added is returned. * * @param type The container type. * @return The child container of the given type, or null if no such child exists. */ - public ContainerAtom getContainerAtomOfType(int type) { + public @Nullable ContainerAtom getContainerAtomOfType(int type) { int childrenSize = containerChildren.size(); for (int i = 0; i < childrenSize; i++) { ContainerAtom atom = containerChildren.get(i); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index a2b787d6b0..fe79185697 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -43,6 +43,9 @@ import java.util.List; */ /* package */ final class AtomParsers { + /** Thrown if an edit list couldn't be applied. */ + public static final class UnhandledEditListException extends ParserException {} + private static final String TAG = "AtomParsers"; private static final int TYPE_vide = Util.getIntegerCodeForString("vide"); @@ -117,10 +120,12 @@ import java.util.List; * @param stblAtom stbl (sample table) atom to decode. * @param gaplessInfoHolder Holder to populate with gapless playback information. * @return Sample table described by the stbl atom. - * @throws ParserException If the resulting sample sequence does not contain a sync sample. + * @throws UnhandledEditListException Thrown if the edit list can't be applied. + * @throws ParserException Thrown if the stbl atom can't be parsed. */ - public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom, - GaplessInfoHolder gaplessInfoHolder) throws ParserException { + public static TrackSampleTable parseStbl( + Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder) + throws ParserException { SampleSizeBox sampleSizeBox; Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz); if (stszAtom != null) { @@ -136,7 +141,13 @@ import java.util.List; int sampleCount = sampleSizeBox.getSampleCount(); if (sampleCount == 0) { return new TrackSampleTable( - new long[0], new int[0], 0, new long[0], new int[0], C.TIME_UNSET); + track, + /* offsets= */ new long[0], + /* sizes= */ new int[0], + /* maximumSize= */ 0, + /* timestampsUs= */ new long[0], + /* flags= */ new int[0], + /* durationUs= */ C.TIME_UNSET); } // Entries are byte offsets of chunks. @@ -315,7 +326,8 @@ import java.util.List; // There is no edit list, or we are ignoring it as we already have gapless metadata to apply. // This implementation does not support applying both gapless metadata and an edit list. Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); - return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); + return new TrackSampleTable( + track, offsets, sizes, maximumSize, timestamps, flags, durationUs); } // See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a @@ -342,7 +354,8 @@ import java.util.List; gaplessInfoHolder.encoderDelay = (int) encoderDelay; gaplessInfoHolder.encoderPadding = (int) encoderPadding; Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); - return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); + return new TrackSampleTable( + track, offsets, sizes, maximumSize, timestamps, flags, durationUs); } } } @@ -359,7 +372,8 @@ import java.util.List; } durationUs = Util.scaleLargeTimestamp(duration - editStartTime, C.MICROS_PER_SECOND, track.timescale); - return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); + return new TrackSampleTable( + track, offsets, sizes, maximumSize, timestamps, flags, durationUs); } // Omit any sample at the end point of an edit for audio tracks. @@ -409,6 +423,11 @@ import java.util.List; System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count); System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count); } + if (startIndex < endIndex && (editedFlags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) == 0) { + // Applying the edit list would require prerolling from a sync sample. + Log.w(TAG, "Ignoring edit list: edit does not start with a sync sample."); + throw new UnhandledEditListException(); + } for (int j = startIndex; j < endIndex; j++) { long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale); long timeInSegmentUs = @@ -424,20 +443,8 @@ import java.util.List; pts += editDuration; } long editedDurationUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.timescale); - - boolean hasSyncSample = false; - for (int i = 0; i < editedFlags.length && !hasSyncSample; i++) { - hasSyncSample |= (editedFlags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0; - } - if (!hasSyncSample) { - // We don't support edit lists where the edited sample sequence doesn't contain a sync sample. - // Such edit lists are often (although not always) broken, so we ignore it and continue. - Log.w(TAG, "Ignoring edit list: Edited sample sequence does not contain a sync sample."); - Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); - return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); - } - return new TrackSampleTable( + track, editedOffsets, editedSizes, editedMaximumSize, @@ -700,8 +707,18 @@ import java.util.List; throw new IllegalStateException(); } - out.format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, - Format.NO_VALUE, 0, language, Format.NO_VALUE, null, subsampleOffsetUs, initializationData); + out.format = + Format.createTextSampleFormat( + Integer.toString(trackId), + mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* selectionFlags= */ 0, + language, + /* accessibilityChannel= */ Format.NO_VALUE, + /* drmInitData= */ null, + subsampleOffsetUs, + initializationData); } private static void parseVideoSampleEntry(ParsableByteArray parent, int atomType, int position, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java index 8336a280a2..536f70048c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java @@ -108,4 +108,7 @@ import com.google.android.exoplayer2.util.Util; return new Results(offsets, sizes, maximumSize, timestamps, flags, duration); } + private FixedSampleSizeRechunker() { + // Prevent instantiation. + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index d1134dc3f6..12da11fd6b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -50,7 +50,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Stack; import java.util.UUID; /** @@ -58,17 +57,9 @@ import java.util.UUID; */ public final class FragmentedMp4Extractor implements Extractor { - /** - * Factory for {@link FragmentedMp4Extractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { - - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new FragmentedMp4Extractor()}; - } - - }; + /** Factory for {@link FragmentedMp4Extractor} instances. */ + public static final ExtractorsFactory FACTORY = + () -> new Extractor[] {new FragmentedMp4Extractor()}; /** * Flags controlling the behavior of the extractor. @@ -86,24 +77,20 @@ public final class FragmentedMp4Extractor implements Extractor { * This flag does nothing if the stream is not a video stream. */ public static final int FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1; - /** - * Flag to ignore any tfdt boxes in the stream. - */ - public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 2; + /** Flag to ignore any tfdt boxes in the stream. */ + public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 1 << 1; // 2 /** * Flag to indicate that the extractor should output an event message metadata track. Any event * messages in the stream will be delivered as samples to this track. */ - public static final int FLAG_ENABLE_EMSG_TRACK = 4; + public static final int FLAG_ENABLE_EMSG_TRACK = 1 << 2; // 4 /** * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4 * container. */ - private static final int FLAG_SIDELOADED = 8; - /** - * Flag to ignore any edit lists in the stream. - */ - public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 16; + private static final int FLAG_SIDELOADED = 1 << 3; // 8 + /** Flag to ignore any edit lists in the stream. */ + public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 1 << 4; // 16 private static final String TAG = "FragmentedMp4Extractor"; private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); @@ -141,7 +128,7 @@ public final class FragmentedMp4Extractor implements Extractor { // Parser state. private final ParsableByteArray atomHeader; private final byte[] extendedTypeScratch; - private final Stack containerAtoms; + private final ArrayDeque containerAtoms; private final ArrayDeque pendingMetadataSampleInfos; private final @Nullable TrackOutput additionalEmsgTrackOutput; @@ -202,8 +189,7 @@ public final class FragmentedMp4Extractor implements Extractor { @Nullable TimestampAdjuster timestampAdjuster, @Nullable Track sideloadedTrack, @Nullable DrmInitData sideloadedDrmInitData) { - this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, - Collections.emptyList()); + this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, Collections.emptyList()); } /** @@ -257,7 +243,7 @@ public final class FragmentedMp4Extractor implements Extractor { nalPrefix = new ParsableByteArray(5); nalBuffer = new ParsableByteArray(); extendedTypeScratch = new byte[16]; - containerAtoms = new Stack<>(); + containerAtoms = new ArrayDeque<>(); pendingMetadataSampleInfos = new ArrayDeque<>(); trackBundles = new SparseArray<>(); durationUs = C.TIME_UNSET; @@ -390,7 +376,7 @@ public final class FragmentedMp4Extractor implements Extractor { if (shouldParseContainerAtom(atomType)) { long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE; - containerAtoms.add(new ContainerAtom(atomType, endPosition)); + containerAtoms.push(new ContainerAtom(atomType, endPosition)); if (atomSize == atomHeaderBytesRead) { processAtomEnded(endPosition); } else { @@ -500,7 +486,7 @@ public final class FragmentedMp4Extractor implements Extractor { for (int i = 0; i < trackCount; i++) { Track track = tracks.valueAt(i); TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type)); - trackBundle.init(track, defaultSampleValuesArray.get(track.id)); + trackBundle.init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id)); trackBundles.put(track.id, trackBundle); durationUs = Math.max(durationUs, track.durationUs); } @@ -510,11 +496,23 @@ public final class FragmentedMp4Extractor implements Extractor { Assertions.checkState(trackBundles.size() == trackCount); for (int i = 0; i < trackCount; i++) { Track track = tracks.valueAt(i); - trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id)); + trackBundles + .get(track.id) + .init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id)); } } } + private DefaultSampleValues getDefaultSampleValues( + SparseArray defaultSampleValuesArray, int trackId) { + if (defaultSampleValuesArray.size() == 1) { + // Ignore track id if there is only one track to cope with non-matching track indices. + // See https://github.com/google/ExoPlayer/issues/4477. + return defaultSampleValuesArray.valueAt(/* index= */ 0); + } + return Assertions.checkNotNull(defaultSampleValuesArray.get(trackId)); + } + private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { parseMoof(moof, trackBundles, flags, extendedTypeScratch); // If drm init data is sideloaded, we ignore pssh boxes. @@ -587,10 +585,13 @@ public final class FragmentedMp4Extractor implements Extractor { // Output the sample metadata. if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) { + long sampleTimeUs = segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs; + if (timestampAdjuster != null) { + sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); + } for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { emsgTrackOutput.sampleMetadata( - segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs, - C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0 /* offset */, null); + sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, /* offset= */ 0, null); } } else { // We need the first sample timestamp in the segment before we can output the metadata. @@ -643,7 +644,7 @@ public final class FragmentedMp4Extractor implements Extractor { private static void parseTraf(ContainerAtom traf, SparseArray trackBundleArray, @Flags int flags, byte[] extendedTypeScratch) throws ParserException { LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd); - TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray, flags); + TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray); if (trackBundle == null) { return; } @@ -729,7 +730,7 @@ public final class FragmentedMp4Extractor implements Extractor { private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz, TrackFragment out) throws ParserException { - int vectorSize = encryptionBox.initializationVectorSize; + int vectorSize = encryptionBox.perSampleIvSize; saiz.setPosition(Atom.HEADER_SIZE); int fullAtom = saiz.readInt(); int flags = Atom.parseFullAtomFlags(fullAtom); @@ -794,13 +795,13 @@ public final class FragmentedMp4Extractor implements Extractor { * @return The {@link TrackBundle} to which the {@link TrackFragment} belongs, or null if the tfhd * does not refer to any {@link TrackBundle}. */ - private static TrackBundle parseTfhd(ParsableByteArray tfhd, - SparseArray trackBundles, int flags) { + private static TrackBundle parseTfhd( + ParsableByteArray tfhd, SparseArray trackBundles) { tfhd.setPosition(Atom.HEADER_SIZE); int fullAtom = tfhd.readInt(); int atomFlags = Atom.parseFullAtomFlags(fullAtom); int trackId = tfhd.readInt(); - TrackBundle trackBundle = trackBundles.get((flags & FLAG_SIDELOADED) == 0 ? trackId : 0); + TrackBundle trackBundle = getTrackBundle(trackBundles, trackId); if (trackBundle == null) { return null; } @@ -825,6 +826,17 @@ public final class FragmentedMp4Extractor implements Extractor { return trackBundle; } + private static @Nullable TrackBundle getTrackBundle( + SparseArray trackBundles, int trackId) { + if (trackBundles.size() == 1) { + // Ignore track id if there is only one track. This is either because we have a side-loaded + // track (flag FLAG_SIDELOADED) or to cope with non-matching track indices (see + // https://github.com/google/ExoPlayer/issues/4083). + return trackBundles.valueAt(/* index= */ 0); + } + return trackBundles.get(trackId); + } + /** * Parses a tfdt atom (defined in 14496-12). * @@ -1186,6 +1198,10 @@ public final class FragmentedMp4Extractor implements Extractor { Track track = currentTrackBundle.track; TrackOutput output = currentTrackBundle.output; int sampleIndex = currentTrackBundle.currentSampleIndex; + long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L; + if (timestampAdjuster != null) { + sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); + } if (track.nalUnitLengthFieldLength != 0) { // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case // they're only 1 or 2 bytes long. @@ -1226,8 +1242,7 @@ public final class FragmentedMp4Extractor implements Extractor { // If the format is H.265/HEVC the NAL unit header has two bytes so skip one more byte. nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0); nalBuffer.setLimit(unescapedLength); - CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalBuffer, - cea608TrackOutputs); + CeaUtil.consume(sampleTimeUs, nalBuffer, cea608TrackOutputs); } else { // Write the payload of the NAL unit. writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); @@ -1243,21 +1258,14 @@ public final class FragmentedMp4Extractor implements Extractor { } } - long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L; - if (timestampAdjuster != null) { - sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); - } - @C.BufferFlags int sampleFlags = fragment.sampleIsSyncFrameTable[sampleIndex] ? C.BUFFER_FLAG_KEY_FRAME : 0; // Encryption data. TrackOutput.CryptoData cryptoData = null; - if (fragment.definesEncryptionData) { + TrackEncryptionBox encryptionBox = currentTrackBundle.getEncryptionBoxIfEncrypted(); + if (encryptionBox != null) { sampleFlags |= C.BUFFER_FLAG_ENCRYPTED; - TrackEncryptionBox encryptionBox = fragment.trackEncryptionBox != null - ? fragment.trackEncryptionBox - : track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); cryptoData = encryptionBox.cryptoData; } @@ -1276,10 +1284,17 @@ public final class FragmentedMp4Extractor implements Extractor { while (!pendingMetadataSampleInfos.isEmpty()) { MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst(); pendingMetadataSampleBytes -= sampleInfo.size; + long metadataTimeUs = sampleTimeUs + sampleInfo.presentationTimeDeltaUs; + if (timestampAdjuster != null) { + metadataTimeUs = timestampAdjuster.adjustSampleTimestamp(metadataTimeUs); + } for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { emsgTrackOutput.sampleMetadata( - sampleTimeUs + sampleInfo.presentationTimeDeltaUs, - C.BUFFER_FLAG_KEY_FRAME, sampleInfo.size, pendingMetadataSampleBytes, null); + metadataTimeUs, + C.BUFFER_FLAG_KEY_FRAME, + sampleInfo.size, + pendingMetadataSampleBytes, + null); } } } @@ -1454,16 +1469,16 @@ public final class FragmentedMp4Extractor implements Extractor { * @return The number of written bytes. */ public int outputSampleEncryptionData() { - if (!fragment.definesEncryptionData) { + TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); + if (encryptionBox == null) { return 0; } - TrackEncryptionBox encryptionBox = getEncryptionBox(); ParsableByteArray initializationVectorData; int vectorSize; - if (encryptionBox.initializationVectorSize != 0) { + if (encryptionBox.perSampleIvSize != 0) { initializationVectorData = fragment.sampleEncryptionData; - vectorSize = encryptionBox.initializationVectorSize; + vectorSize = encryptionBox.perSampleIvSize; } else { // The default initialization vector should be used. byte[] initVectorData = encryptionBox.defaultInitializationVector; @@ -1472,7 +1487,7 @@ public final class FragmentedMp4Extractor implements Extractor { vectorSize = initVectorData.length; } - boolean subsampleEncryption = fragment.sampleHasSubsampleEncryptionTable[currentSampleIndex]; + boolean subsampleEncryption = fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex); // Write the signal byte, containing the vector size and the subsample encryption flag. encryptionSignalByte.data[0] = (byte) (vectorSize | (subsampleEncryption ? 0x80 : 0)); @@ -1495,25 +1510,27 @@ public final class FragmentedMp4Extractor implements Extractor { /** Skips the encryption data for the current sample. */ private void skipSampleEncryptionData() { - if (!fragment.definesEncryptionData) { + TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); + if (encryptionBox == null) { return; } ParsableByteArray sampleEncryptionData = fragment.sampleEncryptionData; - TrackEncryptionBox encryptionBox = getEncryptionBox(); - if (encryptionBox.initializationVectorSize != 0) { - sampleEncryptionData.skipBytes(encryptionBox.initializationVectorSize); + if (encryptionBox.perSampleIvSize != 0) { + sampleEncryptionData.skipBytes(encryptionBox.perSampleIvSize); } - if (fragment.sampleHasSubsampleEncryptionTable[currentSampleIndex]) { + if (fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex)) { sampleEncryptionData.skipBytes(6 * sampleEncryptionData.readUnsignedShort()); } } - private TrackEncryptionBox getEncryptionBox() { + private TrackEncryptionBox getEncryptionBoxIfEncrypted() { int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex; - return fragment.trackEncryptionBox != null - ? fragment.trackEncryptionBox - : track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex); + TrackEncryptionBox encryptionBox = + fragment.trackEncryptionBox != null + ? fragment.trackEncryptionBox + : track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex); + return encryptionBox != null && encryptionBox.isEncrypted ? encryptionBox : null; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java index fed1694925..ed7c539118 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -15,11 +15,13 @@ */ package com.google.android.exoplayer2.extractor.mp4; +import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.Id3Frame; +import com.google.android.exoplayer2.metadata.id3.InternalFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -68,6 +70,8 @@ import com.google.android.exoplayer2.util.Util; // Type for items that are intended for internal use by the player. private static final int TYPE_INTERNAL = Util.getIntegerCodeForString("----"); + private static final int PICTURE_TYPE_FRONT_COVER = 3; + // Standard genres. private static final String[] STANDARD_GENRES = new String[] { // These are the official ID3v1 genres. @@ -103,13 +107,13 @@ import com.google.android.exoplayer2.util.Util; /** * Parses a single ilst element from a {@link ParsableByteArray}. The element is read starting - * from the current position of the {@link ParsableByteArray}, and the position is advanced by - * the size of the element. The position is advanced even if the element's type is unrecognized. + * from the current position of the {@link ParsableByteArray}, and the position is advanced by the + * size of the element. The position is advanced even if the element's type is unrecognized. * * @param ilst Holds the data to be parsed. * @return The parsed element, or null if the element's type was not recognized. */ - public static Metadata.Entry parseIlstElement(ParsableByteArray ilst) { + public static @Nullable Metadata.Entry parseIlstElement(ParsableByteArray ilst) { int position = ilst.getPosition(); int endPosition = position + ilst.readInt(); int type = ilst.readInt(); @@ -181,20 +185,20 @@ import com.google.android.exoplayer2.util.Util; } } - private static TextInformationFrame parseTextAttribute(int type, String id, - ParsableByteArray data) { + private static @Nullable TextInformationFrame parseTextAttribute( + int type, String id, ParsableByteArray data) { int atomSize = data.readInt(); int atomType = data.readInt(); if (atomType == Atom.TYPE_data) { data.skipBytes(8); // version (1), flags (3), empty (4) String value = data.readNullTerminatedString(atomSize - 16); - return new TextInformationFrame(id, null, value); + return new TextInformationFrame(id, /* description= */ null, value); } Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type)); return null; } - private static CommentFrame parseCommentAttribute(int type, ParsableByteArray data) { + private static @Nullable CommentFrame parseCommentAttribute(int type, ParsableByteArray data) { int atomSize = data.readInt(); int atomType = data.readInt(); if (atomType == Atom.TYPE_data) { @@ -206,22 +210,27 @@ import com.google.android.exoplayer2.util.Util; return null; } - private static Id3Frame parseUint8Attribute(int type, String id, ParsableByteArray data, - boolean isTextInformationFrame, boolean isBoolean) { + private static @Nullable Id3Frame parseUint8Attribute( + int type, + String id, + ParsableByteArray data, + boolean isTextInformationFrame, + boolean isBoolean) { int value = parseUint8AttributeValue(data); if (isBoolean) { value = Math.min(1, value); } if (value >= 0) { - return isTextInformationFrame ? new TextInformationFrame(id, null, Integer.toString(value)) + return isTextInformationFrame + ? new TextInformationFrame(id, /* description= */ null, Integer.toString(value)) : new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value)); } Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type)); return null; } - private static TextInformationFrame parseIndexAndCountAttribute(int type, String attributeName, - ParsableByteArray data) { + private static @Nullable TextInformationFrame parseIndexAndCountAttribute( + int type, String attributeName, ParsableByteArray data) { int atomSize = data.readInt(); int atomType = data.readInt(); if (atomType == Atom.TYPE_data && atomSize >= 22) { @@ -233,25 +242,26 @@ import com.google.android.exoplayer2.util.Util; if (count > 0) { value += "/" + count; } - return new TextInformationFrame(attributeName, null, value); + return new TextInformationFrame(attributeName, /* description= */ null, value); } } Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type)); return null; } - private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) { + private static @Nullable TextInformationFrame parseStandardGenreAttribute( + ParsableByteArray data) { int genreCode = parseUint8AttributeValue(data); String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length) ? STANDARD_GENRES[genreCode - 1] : null; if (genreString != null) { - return new TextInformationFrame("TCON", null, genreString); + return new TextInformationFrame("TCON", /* description= */ null, genreString); } Log.w(TAG, "Failed to parse standard genre code"); return null; } - private static ApicFrame parseCoverArt(ParsableByteArray data) { + private static @Nullable ApicFrame parseCoverArt(ParsableByteArray data) { int atomSize = data.readInt(); int atomType = data.readInt(); if (atomType == Atom.TYPE_data) { @@ -265,13 +275,18 @@ import com.google.android.exoplayer2.util.Util; data.skipBytes(4); // empty (4) byte[] pictureData = new byte[atomSize - 16]; data.readBytes(pictureData, 0, pictureData.length); - return new ApicFrame(mimeType, null, 3 /* Cover (front) */, pictureData); + return new ApicFrame( + mimeType, + /* description= */ null, + /* pictureType= */ PICTURE_TYPE_FRONT_COVER, + pictureData); } Log.w(TAG, "Failed to parse cover art attribute"); return null; } - private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) { + private static @Nullable Id3Frame parseInternalAttribute( + ParsableByteArray data, int endPosition) { String domain = null; String name = null; int dataAtomPosition = -1; @@ -293,14 +308,13 @@ import com.google.android.exoplayer2.util.Util; data.skipBytes(atomSize - 12); } } - if (!"com.apple.iTunes".equals(domain) || !"iTunSMPB".equals(name) || dataAtomPosition == -1) { - // We're only interested in iTunSMPB. + if (domain == null || name == null || dataAtomPosition == -1) { return null; } data.setPosition(dataAtomPosition); data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4) String value = data.readNullTerminatedString(dataAtomSize - 16); - return new CommentFrame(LANGUAGE_UNDEFINED, name, value); + return new InternalFrame(domain, name, value); } private static int parseUint8AttributeValue(ParsableByteArray data) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 75bd2c16ee..5bb5e214c9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -37,26 +37,17 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; -import java.util.Stack; /** * Extracts data from the MP4 container format. */ public final class Mp4Extractor implements Extractor, SeekMap { - /** - * Factory for {@link Mp4Extractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { - - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new Mp4Extractor()}; - } - - }; + /** Factory for {@link Mp4Extractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Mp4Extractor()}; /** * Flags controlling the behavior of the extractor. @@ -101,7 +92,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { private final ParsableByteArray nalLength; private final ParsableByteArray atomHeader; - private final Stack containerAtoms; + private final ArrayDeque containerAtoms; @State private int parserState; private int atomType; @@ -137,7 +128,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { public Mp4Extractor(@Flags int flags) { this.flags = flags; atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); - containerAtoms = new Stack<>(); + containerAtoms = new ArrayDeque<>(); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalLength = new ParsableByteArray(4); sampleTrackIndex = C.INDEX_UNSET; @@ -303,7 +294,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { if (shouldParseContainerAtom(atomType)) { long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead; - containerAtoms.add(new ContainerAtom(atomType, endPosition)); + containerAtoms.push(new ContainerAtom(atomType, endPosition)); if (atomSize == atomHeaderBytesRead) { processAtomEnded(endPosition); } else { @@ -391,25 +382,21 @@ public final class Mp4Extractor implements Extractor, SeekMap { } } - for (int i = 0; i < moov.containerChildren.size(); i++) { - Atom.ContainerAtom atom = moov.containerChildren.get(i); - if (atom.type != Atom.TYPE_trak) { - continue; - } - - Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), - C.TIME_UNSET, null, (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, isQuickTime); - if (track == null) { - continue; - } - - Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia) - .getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl); - TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); - if (trackSampleTable.sampleCount == 0) { - continue; - } + boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0; + ArrayList trackSampleTables; + try { + trackSampleTables = getTrackSampleTables(moov, gaplessInfoHolder, ignoreEditLists); + } catch (AtomParsers.UnhandledEditListException e) { + // Discard gapless info as we aren't able to handle corresponding edits. + gaplessInfoHolder = new GaplessInfoHolder(); + trackSampleTables = + getTrackSampleTables(moov, gaplessInfoHolder, /* ignoreEditLists= */ true); + } + int trackCount = trackSampleTables.size(); + for (int i = 0; i < trackCount; i++) { + TrackSampleTable trackSampleTable = trackSampleTables.get(i); + Track track = trackSampleTable.track; Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i, track.type)); // Each sample has up to three bytes of overhead for the start code that replaces its length. @@ -445,6 +432,39 @@ public final class Mp4Extractor implements Extractor, SeekMap { extractorOutput.seekMap(this); } + private ArrayList getTrackSampleTables( + ContainerAtom moov, GaplessInfoHolder gaplessInfoHolder, boolean ignoreEditLists) + throws ParserException { + ArrayList trackSampleTables = new ArrayList<>(); + for (int i = 0; i < moov.containerChildren.size(); i++) { + Atom.ContainerAtom atom = moov.containerChildren.get(i); + if (atom.type != Atom.TYPE_trak) { + continue; + } + Track track = + AtomParsers.parseTrak( + atom, + moov.getLeafAtomOfType(Atom.TYPE_mvhd), + /* duration= */ C.TIME_UNSET, + /* drmInitData= */ null, + ignoreEditLists, + isQuickTime); + if (track == null) { + continue; + } + Atom.ContainerAtom stblAtom = + atom.getContainerAtomOfType(Atom.TYPE_mdia) + .getContainerAtomOfType(Atom.TYPE_minf) + .getContainerAtomOfType(Atom.TYPE_stbl); + TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); + if (trackSampleTable.sampleCount == 0) { + continue; + } + trackSampleTables.add(trackSampleTable); + } + return trackSampleTables; + } + /** * Attempts to extract the next sample in the current mdat atom for the specified track. *

    diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java index 84513ef4d3..983c23dc3d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java @@ -49,28 +49,28 @@ public final class PsshAtomUtil { * @param data The scheme specific data. * @return The PSSH atom. */ + @SuppressWarnings("ParameterNotNullable") public static byte[] buildPsshAtom( UUID systemId, @Nullable UUID[] keyIds, @Nullable byte[] data) { - boolean buildV1Atom = keyIds != null; int dataLength = data != null ? data.length : 0; int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* SystemId */ + 4 /* DataSize */ + dataLength; - if (buildV1Atom) { + if (keyIds != null) { psshBoxLength += 4 /* KID_count */ + (keyIds.length * 16) /* KIDs */; } ByteBuffer psshBox = ByteBuffer.allocate(psshBoxLength); psshBox.putInt(psshBoxLength); psshBox.putInt(Atom.TYPE_pssh); - psshBox.putInt(buildV1Atom ? 0x01000000 : 0 /* version=(buildV1Atom ? 1 : 0), flags=0 */); + psshBox.putInt(keyIds != null ? 0x01000000 : 0 /* version=(buildV1Atom ? 1 : 0), flags=0 */); psshBox.putLong(systemId.getMostSignificantBits()); psshBox.putLong(systemId.getLeastSignificantBits()); - if (buildV1Atom) { + if (keyIds != null) { psshBox.putInt(keyIds.length); for (UUID keyId : keyIds) { psshBox.putLong(keyId.getMostSignificantBits()); psshBox.putLong(keyId.getLeastSignificantBits()); } } - if (dataLength != 0) { + if (data != null && data.length != 0) { psshBox.putInt(data.length); psshBox.put(data); } // Else the last 4 bytes are a 0 DataSize. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java index d39aae0c5f..54207f351f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java @@ -45,33 +45,36 @@ public final class TrackEncryptionBox { */ public final TrackOutput.CryptoData cryptoData; - /** - * The initialization vector size in bytes for the samples in the corresponding sample group. - */ - public final int initializationVectorSize; + /** The initialization vector size in bytes for the samples in the corresponding sample group. */ + public final int perSampleIvSize; /** - * If {@link #initializationVectorSize} is 0, holds the default initialization vector as defined - * in the track encryption box or sample group description box. Null otherwise. + * If {@link #perSampleIvSize} is 0, holds the default initialization vector as defined in the + * track encryption box or sample group description box. Null otherwise. */ public final byte[] defaultInitializationVector; /** * @param isEncrypted See {@link #isEncrypted}. * @param schemeType See {@link #schemeType}. - * @param initializationVectorSize See {@link #initializationVectorSize}. + * @param perSampleIvSize See {@link #perSampleIvSize}. * @param keyId See {@link TrackOutput.CryptoData#encryptionKey}. * @param defaultEncryptedBlocks See {@link TrackOutput.CryptoData#encryptedBlocks}. * @param defaultClearBlocks See {@link TrackOutput.CryptoData#clearBlocks}. * @param defaultInitializationVector See {@link #defaultInitializationVector}. */ - public TrackEncryptionBox(boolean isEncrypted, @Nullable String schemeType, - int initializationVectorSize, byte[] keyId, int defaultEncryptedBlocks, - int defaultClearBlocks, @Nullable byte[] defaultInitializationVector) { - Assertions.checkArgument(initializationVectorSize == 0 ^ defaultInitializationVector == null); + public TrackEncryptionBox( + boolean isEncrypted, + @Nullable String schemeType, + int perSampleIvSize, + byte[] keyId, + int defaultEncryptedBlocks, + int defaultClearBlocks, + @Nullable byte[] defaultInitializationVector) { + Assertions.checkArgument(perSampleIvSize == 0 ^ defaultInitializationVector == null); this.isEncrypted = isEncrypted; this.schemeType = schemeType; - this.initializationVectorSize = initializationVectorSize; + this.perSampleIvSize = perSampleIvSize; this.defaultInitializationVector = defaultInitializationVector; cryptoData = new TrackOutput.CryptoData(schemeToCryptoMode(schemeType), keyId, defaultEncryptedBlocks, defaultClearBlocks); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java index 5ac673d037..51ec2bf282 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java @@ -190,4 +190,8 @@ import java.io.IOException; return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index]; } + /** Returns whether the sample at the given index has a subsample encryption table. */ + public boolean sampleHasSubsampleEncryptionTable(int index) { + return definesEncryptionData && sampleHasSubsampleEncryptionTable[index]; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java index 9f77c49664..56851fc1e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java @@ -24,29 +24,19 @@ import com.google.android.exoplayer2.util.Util; */ /* package */ final class TrackSampleTable { - /** - * Number of samples. - */ + /** The track corresponding to this sample table. */ + public final Track track; + /** Number of samples. */ public final int sampleCount; - /** - * Sample offsets in bytes. - */ + /** Sample offsets in bytes. */ public final long[] offsets; - /** - * Sample sizes in bytes. - */ + /** Sample sizes in bytes. */ public final int[] sizes; - /** - * Maximum sample size in {@link #sizes}. - */ + /** Maximum sample size in {@link #sizes}. */ public final int maximumSize; - /** - * Sample timestamps in microseconds. - */ + /** Sample timestamps in microseconds. */ public final long[] timestampsUs; - /** - * Sample flags. - */ + /** Sample flags. */ public final int[] flags; /** * The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample @@ -55,6 +45,7 @@ import com.google.android.exoplayer2.util.Util; public final long durationUs; public TrackSampleTable( + Track track, long[] offsets, int[] sizes, int maximumSize, @@ -65,6 +56,7 @@ import com.google.android.exoplayer2.util.Util; Assertions.checkArgument(offsets.length == timestampsUs.length); Assertions.checkArgument(flags.length == timestampsUs.length); + this.track = track; this.offsets = offsets; this.sizes = sizes; this.maximumSize = maximumSize; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java index a4d8f97d5b..5e74eab8d4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java @@ -31,17 +31,8 @@ import java.io.IOException; */ public class OggExtractor implements Extractor { - /** - * Factory for {@link OggExtractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { - - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new OggExtractor()}; - } - - }; + /** Factory for {@link OggExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new OggExtractor()}; private static final int MAX_VERIFICATION_BYTES = 8; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java index 8ed8a4a01d..ce3b9ea6ba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java @@ -130,6 +130,6 @@ import java.util.List; } else { length = 10000 << length; } - return frames * length; + return (long) frames * length; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java index 79767a00d8..0235fba272 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java @@ -357,12 +357,12 @@ import java.util.Arrays; for (int i = 0; i < lengthMap.length; i++) { if (isSparse) { if (bitArray.readBit()) { - lengthMap[i] = bitArray.readBits(5) + 1; + lengthMap[i] = (long) (bitArray.readBits(5) + 1); } else { // entry unused lengthMap[i] = 0; } } else { // not sparse - lengthMap[i] = bitArray.readBits(5) + 1; + lengthMap[i] = (long) (bitArray.readBits(5) + 1); } } } else { @@ -392,7 +392,7 @@ import java.util.Arrays; lookupValuesCount = 0; } } else { - lookupValuesCount = entries * dimensions; + lookupValuesCount = (long) entries * dimensions; } // discard (no decoding required yet) bitArray.skipBits((int) (lookupValuesCount * valueBits)); @@ -407,6 +407,10 @@ import java.util.Arrays; return (long) Math.floor(Math.pow(entries, 1.d / dimension)); } + private VorbisUtil() { + // Prevent instantiation. + } + public static final class CodeBook { public final int dimensions; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index bc37277c57..cd806cfe05 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -33,17 +33,8 @@ import java.io.IOException; */ public final class Ac3Extractor implements Extractor { - /** - * Factory for {@link Ac3Extractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { - - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new Ac3Extractor()}; - } - - }; + /** Factory for {@link Ac3Extractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Ac3Extractor()}; /** * The maximum number of bytes to search when sniffing, excluding ID3 information, before giving diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index a0a748660e..ef7b763306 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -15,7 +15,11 @@ */ package com.google.android.exoplayer2.extractor.ts; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -23,50 +27,90 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Extracts data from AAC bit streams with ADTS framing. */ public final class AdtsExtractor implements Extractor { + /** Factory for {@link AdtsExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AdtsExtractor()}; + + /** Flags controlling the behavior of the extractor. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}) + public @interface Flags {} /** - * Factory for {@link AdtsExtractor} instances. + * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would + * otherwise not be possible. + * + *

    Note that this approach may result in approximated stream duration and seek position that + * are not precise, especially when the stream bitrate varies a lot. */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { + public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new AdtsExtractor()}; - } - - }; - - private static final int MAX_PACKET_SIZE = 200; + private static final int MAX_PACKET_SIZE = 2 * 1024; private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); /** * The maximum number of bytes to search when sniffing, excluding the header, before giving up. * Frame sizes are represented by 13-bit fields, so expect a valid frame in the first 8192 bytes. */ private static final int MAX_SNIFF_BYTES = 8 * 1024; + /** + * The maximum number of frames to use when calculating the average frame size for constant + * bitrate seeking. + */ + private static final int NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE = 1000; + + private final @Flags int flags; - private final long firstSampleTimestampUs; private final AdtsReader reader; private final ParsableByteArray packetBuffer; + private final ParsableByteArray scratch; + private final ParsableBitArray scratchBits; + private final long firstStreamSampleTimestampUs; + private @Nullable ExtractorOutput extractorOutput; + + private long firstSampleTimestampUs; + private long firstFramePosition; + private int averageFrameSize; + private boolean hasCalculatedAverageFrameSize; private boolean startedPacket; + private boolean hasOutputSeekMap; public AdtsExtractor() { this(0); } - public AdtsExtractor(long firstSampleTimestampUs) { - this.firstSampleTimestampUs = firstSampleTimestampUs; + public AdtsExtractor(long firstStreamSampleTimestampUs) { + this(/* firstStreamSampleTimestampUs= */ firstStreamSampleTimestampUs, /* flags= */ 0); + } + + /** + * @param firstStreamSampleTimestampUs The timestamp to be used for the first sample of the stream + * output from this extractor. + * @param flags Flags that control the extractor's behavior. + */ + public AdtsExtractor(long firstStreamSampleTimestampUs, @Flags int flags) { + this.firstStreamSampleTimestampUs = firstStreamSampleTimestampUs; + this.firstSampleTimestampUs = firstStreamSampleTimestampUs; + this.flags = flags; reader = new AdtsReader(true); packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); + averageFrameSize = C.LENGTH_UNSET; + firstFramePosition = C.POSITION_UNSET; + scratch = new ParsableByteArray(10); + scratchBits = new ParsableBitArray(scratch.data); } // Extractor implementation. @@ -74,41 +118,26 @@ public final class AdtsExtractor implements Extractor { @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { // Skip any ID3 headers. - ParsableByteArray scratch = new ParsableByteArray(10); - ParsableBitArray scratchBits = new ParsableBitArray(scratch.data); - int startPosition = 0; - while (true) { - input.peekFully(scratch.data, 0, 10); - scratch.setPosition(0); - if (scratch.readUnsignedInt24() != ID3_TAG) { - break; - } - scratch.skipBytes(3); - int length = scratch.readSynchSafeInt(); - startPosition += 10 + length; - input.advancePeekPosition(length); - } - input.resetPeekPosition(); - input.advancePeekPosition(startPosition); + int startPosition = peekId3Header(input); // Try to find four or more consecutive AAC audio frames, exceeding the MPEG TS packet size. int headerPosition = startPosition; - int validFramesSize = 0; + int totalValidFramesSize = 0; int validFramesCount = 0; while (true) { input.peekFully(scratch.data, 0, 2); scratch.setPosition(0); int syncBytes = scratch.readUnsignedShort(); - if ((syncBytes & 0xFFF6) != 0xFFF0) { + if (!AdtsReader.isAdtsSyncWord(syncBytes)) { validFramesCount = 0; - validFramesSize = 0; + totalValidFramesSize = 0; input.resetPeekPosition(); if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) { return false; } input.advancePeekPosition(headerPosition); } else { - if (++validFramesCount >= 4 && validFramesSize > 188) { + if (++validFramesCount >= 4 && totalValidFramesSize > TsExtractor.TS_PACKET_SIZE) { return true; } @@ -121,22 +150,23 @@ public final class AdtsExtractor implements Extractor { return false; } input.advancePeekPosition(frameSize - 6); - validFramesSize += frameSize; + totalValidFramesSize += frameSize; } } } @Override public void init(ExtractorOutput output) { + this.extractorOutput = output; reader.createTracks(output, new TrackIdGenerator(0, 1)); output.endTracks(); - output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); } @Override public void seek(long position, long timeUs) { startedPacket = false; reader.seek(); + firstSampleTimestampUs = firstStreamSampleTimestampUs + timeUs; } @Override @@ -147,8 +177,17 @@ public final class AdtsExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + long inputLength = input.getLength(); + boolean canUseConstantBitrateSeeking = + (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0 && inputLength != C.LENGTH_UNSET; + if (canUseConstantBitrateSeeking) { + calculateAverageFrameSize(input); + } + int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE); - if (bytesRead == C.RESULT_END_OF_INPUT) { + boolean readEndOfStream = bytesRead == RESULT_END_OF_INPUT; + maybeOutputSeekMap(inputLength, canUseConstantBitrateSeeking, readEndOfStream); + if (readEndOfStream) { return RESULT_END_OF_INPUT; } @@ -167,4 +206,117 @@ public final class AdtsExtractor implements Extractor { return RESULT_CONTINUE; } + private int peekId3Header(ExtractorInput input) throws IOException, InterruptedException { + int firstFramePosition = 0; + while (true) { + input.peekFully(scratch.data, 0, 10); + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != ID3_TAG) { + break; + } + scratch.skipBytes(3); + int length = scratch.readSynchSafeInt(); + firstFramePosition += 10 + length; + input.advancePeekPosition(length); + } + input.resetPeekPosition(); + input.advancePeekPosition(firstFramePosition); + if (this.firstFramePosition == C.POSITION_UNSET) { + this.firstFramePosition = firstFramePosition; + } + return firstFramePosition; + } + + private void maybeOutputSeekMap( + long inputLength, boolean canUseConstantBitrateSeeking, boolean readEndOfStream) { + if (hasOutputSeekMap) { + return; + } + boolean useConstantBitrateSeeking = canUseConstantBitrateSeeking && averageFrameSize > 0; + if (useConstantBitrateSeeking + && reader.getSampleDurationUs() == C.TIME_UNSET + && !readEndOfStream) { + // Wait for the sampleDurationUs to be available, or for the end of the stream to be reached, + // before creating seek map. + return; + } + + ExtractorOutput extractorOutput = Assertions.checkNotNull(this.extractorOutput); + if (useConstantBitrateSeeking && reader.getSampleDurationUs() != C.TIME_UNSET) { + extractorOutput.seekMap(getConstantBitrateSeekMap(inputLength)); + } else { + extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); + } + hasOutputSeekMap = true; + } + + private void calculateAverageFrameSize(ExtractorInput input) + throws IOException, InterruptedException { + if (hasCalculatedAverageFrameSize) { + return; + } + averageFrameSize = C.LENGTH_UNSET; + input.resetPeekPosition(); + if (input.getPosition() == 0) { + // Skip any ID3 headers. + peekId3Header(input); + } + + int numValidFrames = 0; + long totalValidFramesSize = 0; + while (input.peekFully( + scratch.data, /* offset= */ 0, /* length= */ 2, /* allowEndOfInput= */ true)) { + scratch.setPosition(0); + int syncBytes = scratch.readUnsignedShort(); + if (!AdtsReader.isAdtsSyncWord(syncBytes)) { + // Invalid sync byte pattern. + // Constant bit-rate seeking will probably fail for this stream. + numValidFrames = 0; + break; + } else { + // Read the frame size. + if (!input.peekFully( + scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true)) { + break; + } + scratchBits.setPosition(14); + int currentFrameSize = scratchBits.readBits(13); + // Either the stream is malformed OR we're not parsing an ADTS stream. + if (currentFrameSize <= 6) { + hasCalculatedAverageFrameSize = true; + throw new ParserException("Malformed ADTS stream"); + } + totalValidFramesSize += currentFrameSize; + if (++numValidFrames == NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE) { + break; + } + if (!input.advancePeekPosition(currentFrameSize - 6, /* allowEndOfInput= */ true)) { + break; + } + } + } + input.resetPeekPosition(); + if (numValidFrames > 0) { + averageFrameSize = (int) (totalValidFramesSize / numValidFrames); + } else { + averageFrameSize = C.LENGTH_UNSET; + } + hasCalculatedAverageFrameSize = true; + } + + private SeekMap getConstantBitrateSeekMap(long inputLength) { + int bitrate = getBitrateFromFrameSize(averageFrameSize, reader.getSampleDurationUs()); + return new ConstantBitrateSeekMap(inputLength, firstFramePosition, bitrate, averageFrameSize); + } + + /** + * Returns the stream bitrate, given a frame size and the duration of that frame in microseconds. + * + * @param frameSize The size of each frame in the stream. + * @param durationUsPerFrame The duration of the given frame in microseconds. + * @return The stream bitrate. + */ + private static int getBitrateFromFrameSize(int frameSize, long durationUsPerFrame) { + return (int) ((frameSize * C.BITS_PER_BYTE * C.MICROS_PER_SECOND) / durationUsPerFrame); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index 96b964a4c4..7f6a22b58b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -39,9 +39,10 @@ public final class AdtsReader implements ElementaryStreamReader { private static final String TAG = "AdtsReader"; private static final int STATE_FINDING_SAMPLE = 0; - private static final int STATE_READING_ID3_HEADER = 1; - private static final int STATE_READING_ADTS_HEADER = 2; - private static final int STATE_READING_SAMPLE = 3; + private static final int STATE_CHECKING_ADTS_HEADER = 1; + private static final int STATE_READING_ID3_HEADER = 2; + private static final int STATE_READING_ADTS_HEADER = 3; + private static final int STATE_READING_SAMPLE = 4; private static final int HEADER_SIZE = 5; private static final int CRC_SIZE = 2; @@ -56,6 +57,7 @@ public final class AdtsReader implements ElementaryStreamReader { private static final int ID3_HEADER_SIZE = 10; private static final int ID3_SIZE_OFFSET = 6; private static final byte[] ID3_IDENTIFIER = {'I', 'D', '3'}; + private static final int VERSION_UNSET = -1; private final boolean exposeId3; private final ParsableBitArray adtsScratch; @@ -72,6 +74,14 @@ public final class AdtsReader implements ElementaryStreamReader { private int matchState; private boolean hasCrc; + private boolean foundFirstFrame; + + // Used to verifies sync words + private int firstFrameVersion; + private int firstFrameSampleRateIndex; + + private int currentFrameVersion; + private int currentFrameSampleRateIndex; // Used when parsing the header. private boolean hasOutputFormat; @@ -99,13 +109,21 @@ public final class AdtsReader implements ElementaryStreamReader { adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE)); setFindingSampleState(); + firstFrameVersion = VERSION_UNSET; + firstFrameSampleRateIndex = C.INDEX_UNSET; + sampleDurationUs = C.TIME_UNSET; this.exposeId3 = exposeId3; this.language = language; } + /** Returns whether an integer matches an ADTS SYNC word. */ + public static boolean isAdtsSyncWord(int candidateSyncWord) { + return (candidateSyncWord & 0xFFF6) == 0xFFF0; + } + @Override public void seek() { - setFindingSampleState(); + resetSync(); } @Override @@ -140,6 +158,9 @@ public final class AdtsReader implements ElementaryStreamReader { parseId3Header(); } break; + case STATE_CHECKING_ADTS_HEADER: + checkAdtsHeader(data); + break; case STATE_READING_ADTS_HEADER: int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE; if (continueRead(data, adtsScratch.data, targetLength)) { @@ -158,6 +179,19 @@ public final class AdtsReader implements ElementaryStreamReader { // Do nothing. } + /** + * Returns the duration in microseconds per sample, or {@link C#TIME_UNSET} if the sample duration + * is not available. + */ + public long getSampleDurationUs() { + return sampleDurationUs; + } + + private void resetSync() { + foundFirstFrame = false; + setFindingSampleState(); + } + /** * Continues a read from the provided {@code source} into a given {@code target}. It's assumed * that the data should be written into {@code target} starting from an offset of zero. @@ -219,6 +253,12 @@ public final class AdtsReader implements ElementaryStreamReader { bytesRead = 0; } + /** Sets the state to STATE_CHECKING_ADTS_HEADER. */ + private void setCheckingAdtsHeaderState() { + state = STATE_CHECKING_ADTS_HEADER; + bytesRead = 0; + } + /** * Locates the next sample start, advancing the position to the byte that immediately follows * identifier. If a sample was not located, the position is advanced to the limit. @@ -231,12 +271,21 @@ public final class AdtsReader implements ElementaryStreamReader { int endOffset = pesBuffer.limit(); while (position < endOffset) { int data = adtsData[position++] & 0xFF; - if (matchState == MATCH_STATE_FF && data >= 0xF0 && data != 0xFF) { - hasCrc = (data & 0x1) == 0; - setReadingAdtsHeaderState(); - pesBuffer.setPosition(position); - return; + if (matchState == MATCH_STATE_FF && isAdtsSyncBytes((byte) 0xFF, (byte) data)) { + if (foundFirstFrame + || checkSyncPositionValid(pesBuffer, /* syncPositionCandidate= */ position - 2)) { + currentFrameVersion = (data & 0x8) >> 3; + hasCrc = (data & 0x1) == 0; + if (!foundFirstFrame) { + setCheckingAdtsHeaderState(); + } else { + setReadingAdtsHeaderState(); + } + pesBuffer.setPosition(position); + return; + } } + switch (matchState | data) { case MATCH_STATE_START | 0xFF: matchState = MATCH_STATE_FF; @@ -264,6 +313,117 @@ public final class AdtsReader implements ElementaryStreamReader { pesBuffer.setPosition(position); } + /** + * Peeks the Adts header of the current frame and checks if it is valid. If the header is valid, + * transition to {@link #STATE_READING_ADTS_HEADER}; else, transition to {@link + * #STATE_FINDING_SAMPLE}. + */ + private void checkAdtsHeader(ParsableByteArray buffer) { + if (buffer.bytesLeft() == 0) { + // Not enough data to check yet, defer this check. + return; + } + // Peek the next byte of buffer into scratch array. + adtsScratch.data[0] = buffer.data[buffer.getPosition()]; + + adtsScratch.setPosition(2); + currentFrameSampleRateIndex = adtsScratch.readBits(4); + if (firstFrameSampleRateIndex != C.INDEX_UNSET + && currentFrameSampleRateIndex != firstFrameSampleRateIndex) { + // Invalid header. + resetSync(); + return; + } + + if (!foundFirstFrame) { + foundFirstFrame = true; + firstFrameVersion = currentFrameVersion; + firstFrameSampleRateIndex = currentFrameSampleRateIndex; + } + setReadingAdtsHeaderState(); + } + + /** + * Returns whether the given syncPositionCandidate is a real SYNC word. + * + *

    SYNC word pattern can occur within AAC data, so we perform a few checks to make sure this is + * really a SYNC word. This includes: + * + *

      + *
    • Checking if MPEG version of this frame matches the first detected version. + *
    • Checking if the sample rate index of this frame matches the first detected sample rate + * index. + *
    • Checking if the bytes immediately after the current package also match a SYNC-word. + *
    + * + * If the buffer runs out of data for any check, optimistically skip that check, because + * AdtsReader consumes each buffer as a whole. We will still run a header validity check later. + */ + private boolean checkSyncPositionValid(ParsableByteArray pesBuffer, int syncPositionCandidate) { + // The SYNC word contains 2 bytes, and the first byte may be in the previously consumed buffer. + // Hence the second byte of the SYNC word may be byte 0 of this buffer, and + // syncPositionCandidate (which indicates position of the first byte of the SYNC word) may be + // -1. + // Since the first byte of the SYNC word is always FF, which does not contain any informational + // bits, we set the byte position to be the second byte in the SYNC word to ensure it's always + // within this buffer. + pesBuffer.setPosition(syncPositionCandidate + 1); + if (!tryRead(pesBuffer, adtsScratch.data, 1)) { + return false; + } + + adtsScratch.setPosition(4); + int currentFrameVersion = adtsScratch.readBits(1); + if (firstFrameVersion != VERSION_UNSET && currentFrameVersion != firstFrameVersion) { + return false; + } + + if (firstFrameSampleRateIndex != C.INDEX_UNSET) { + if (!tryRead(pesBuffer, adtsScratch.data, 1)) { + return true; + } + adtsScratch.setPosition(2); + int currentFrameSampleRateIndex = adtsScratch.readBits(4); + if (currentFrameSampleRateIndex != firstFrameSampleRateIndex) { + return false; + } + pesBuffer.setPosition(syncPositionCandidate + 2); + } + + // Optionally check the byte after this frame matches SYNC word. + + if (!tryRead(pesBuffer, adtsScratch.data, 4)) { + return true; + } + adtsScratch.setPosition(14); + int frameSize = adtsScratch.readBits(13); + if (frameSize <= 6) { + // Not a frame. + return false; + } + int nextSyncPosition = syncPositionCandidate + frameSize; + if (nextSyncPosition + 1 >= pesBuffer.limit()) { + return true; + } + return (isAdtsSyncBytes(pesBuffer.data[nextSyncPosition], pesBuffer.data[nextSyncPosition + 1]) + && (firstFrameVersion == VERSION_UNSET + || ((pesBuffer.data[nextSyncPosition + 1] & 0x8) >> 3) == currentFrameVersion)); + } + + private boolean isAdtsSyncBytes(byte firstByte, byte secondByte) { + int syncWord = (firstByte & 0xFF) << 8 | (secondByte & 0xFF); + return isAdtsSyncWord(syncWord); + } + + /** Reads {@code targetLength} bytes into target, and returns whether the read succeeded. */ + private boolean tryRead(ParsableByteArray source, byte[] target, int targetLength) { + if (source.bytesLeft() < targetLength) { + return false; + } + source.readBytes(target, /* offset= */ 0, targetLength); + return true; + } + /** * Parses the Id3 header. */ @@ -296,12 +456,12 @@ public final class AdtsReader implements ElementaryStreamReader { audioObjectType = 2; } - int sampleRateIndex = adtsScratch.readBits(4); - adtsScratch.skipBits(1); + adtsScratch.skipBits(5); int channelConfig = adtsScratch.readBits(3); - byte[] audioSpecificConfig = CodecSpecificDataUtil.buildAacAudioSpecificConfig( - audioObjectType, sampleRateIndex, channelConfig); + byte[] audioSpecificConfig = + CodecSpecificDataUtil.buildAacAudioSpecificConfig( + audioObjectType, firstFrameSampleRateIndex, channelConfig); Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( audioSpecificConfig); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 2d16b46895..085e3443c1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -19,6 +19,7 @@ import android.support.annotation.IntDef; import android.util.SparseArray; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; +import com.google.android.exoplayer2.text.cea.Cea708InitializationData; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.lang.annotation.Retention; @@ -61,7 +62,10 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact * readers. */ public DefaultTsPayloadReaderFactory(@Flags int flags) { - this(flags, Collections.emptyList()); + this( + flags, + Collections.singletonList( + Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null))); } /** @@ -76,10 +80,6 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact */ public DefaultTsPayloadReaderFactory(@Flags int flags, List closedCaptionFormats) { this.flags = flags; - if (!isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS) && closedCaptionFormats.isEmpty()) { - closedCaptionFormats = Collections.singletonList(Format.createTextSampleFormat(null, - MimeTypes.APPLICATION_CEA608, 0, null)); - } this.closedCaptionFormats = closedCaptionFormats; } @@ -107,7 +107,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact case TsExtractor.TS_STREAM_TYPE_HDMV_DTS: return new PesReader(new DtsReader(esInfo.language)); case TsExtractor.TS_STREAM_TYPE_H262: - return new PesReader(new H262Reader()); + return new PesReader(new H262Reader(buildUserDataReader(esInfo))); case TsExtractor.TS_STREAM_TYPE_H264: return isSet(FLAG_IGNORE_H264_STREAM) ? null : new PesReader(new H264Reader(buildSeiReader(esInfo), @@ -137,8 +137,34 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact * @return A {@link SeiReader} for closed caption tracks. */ private SeiReader buildSeiReader(EsInfo esInfo) { + return new SeiReader(getClosedCaptionFormats(esInfo)); + } + + /** + * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link UserDataReader} for + * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a + * {@link UserDataReader} for the declared formats, or {@link #closedCaptionFormats} if the + * descriptor is not present. + * + * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}. + * @return A {@link UserDataReader} for closed caption tracks. + */ + private UserDataReader buildUserDataReader(EsInfo esInfo) { + return new UserDataReader(getClosedCaptionFormats(esInfo)); + } + + /** + * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link List} of {@link + * #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a {@link + * List} for the declared formats, or {@link #closedCaptionFormats} if the descriptor is + * not present. + * + * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}. + * @return A {@link List} containing list of closed caption formats. + */ + private List getClosedCaptionFormats(EsInfo esInfo) { if (isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS)) { - return new SeiReader(closedCaptionFormats); + return closedCaptionFormats; } ParsableByteArray scratchDescriptorData = new ParsableByteArray(esInfo.descriptorBytes); List closedCaptionFormats = this.closedCaptionFormats; @@ -163,21 +189,42 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact mimeType = MimeTypes.APPLICATION_CEA608; accessibilityChannel = 1; } - closedCaptionFormats.add(Format.createTextSampleFormat(null, mimeType, null, - Format.NO_VALUE, 0, language, accessibilityChannel, null)); - // Skip easy_reader(1), wide_aspect_ratio(1), reserved(14). - scratchDescriptorData.skipBytes(2); + + // easy_reader(1), wide_aspect_ratio(1), reserved(6). + byte flags = (byte) scratchDescriptorData.readUnsignedByte(); + // Skip reserved (8). + scratchDescriptorData.skipBytes(1); + + List initializationData = null; + // The wide_aspect_ratio flag only has meaning for CEA-708. + if (isDigital) { + boolean isWideAspectRatio = (flags & 0x40) != 0; + initializationData = Cea708InitializationData.buildData(isWideAspectRatio); + } + + closedCaptionFormats.add( + Format.createTextSampleFormat( + /* id= */ null, + mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* selectionFlags= */ 0, + language, + accessibilityChannel, + /* drmInitData= */ null, + Format.OFFSET_SAMPLE_RELATIVE, + initializationData)); } } else { // Unknown descriptor. Ignore. } scratchDescriptorData.setPosition(nextDescriptorPosition); } - return new SeiReader(closedCaptionFormats); + + return closedCaptionFormats; } private boolean isSet(@Flags int flag) { return (flags & flag) != 0; } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index a3502a3242..e9827893ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -36,6 +36,7 @@ public final class H262Reader implements ElementaryStreamReader { private static final int START_SEQUENCE_HEADER = 0xB3; private static final int START_EXTENSION = 0xB5; private static final int START_GROUP = 0xB8; + private static final int START_USER_DATA = 0xB2; private String formatId; private TrackOutput output; @@ -48,9 +49,13 @@ public final class H262Reader implements ElementaryStreamReader { private boolean hasOutputFormat; private long frameDurationUs; + private final UserDataReader userDataReader; + private final ParsableByteArray userDataParsable; + // State that should be reset on seek. private final boolean[] prefixFlags; private final CsdBuffer csdBuffer; + private final NalUnitTargetBuffer userData; private long totalBytesWritten; private boolean startedFirstSample; @@ -64,14 +69,29 @@ public final class H262Reader implements ElementaryStreamReader { private boolean sampleHasPicture; public H262Reader() { + this(null); + } + + public H262Reader(UserDataReader userDataReader) { + this.userDataReader = userDataReader; prefixFlags = new boolean[4]; csdBuffer = new CsdBuffer(128); + if (userDataReader != null) { + userData = new NalUnitTargetBuffer(START_USER_DATA, 128); + userDataParsable = new ParsableByteArray(); + } else { + userData = null; + userDataParsable = null; + } } @Override public void seek() { NalUnitUtil.clearPrefixFlags(prefixFlags); csdBuffer.reset(); + if (userDataReader != null) { + userData.reset(); + } totalBytesWritten = 0; startedFirstSample = false; } @@ -81,6 +101,9 @@ public final class H262Reader implements ElementaryStreamReader { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); + if (userDataReader != null) { + userDataReader.createTracks(extractorOutput, idGenerator); + } } @Override @@ -106,16 +129,19 @@ public final class H262Reader implements ElementaryStreamReader { if (!hasOutputFormat) { csdBuffer.onData(dataArray, offset, limit); } + if (userDataReader != null) { + userData.appendToNalUnit(dataArray, offset, limit); + } return; } // We've found a start code with the following value. int startCodeValue = data.data[startCodeOffset + 3] & 0xFF; + // This is the number of bytes from the current offset to the start of the next start + // code. It may be negative if the start code started in the previously consumed data. + int lengthToStartCode = startCodeOffset - offset; if (!hasOutputFormat) { - // This is the number of bytes from the current offset to the start of the next start - // code. It may be negative if the start code started in the previously consumed data. - int lengthToStartCode = startCodeOffset - offset; if (lengthToStartCode > 0) { csdBuffer.onData(dataArray, offset, startCodeOffset); } @@ -130,7 +156,24 @@ public final class H262Reader implements ElementaryStreamReader { hasOutputFormat = true; } } + if (userDataReader != null) { + int bytesAlreadyPassed = 0; + if (lengthToStartCode > 0) { + userData.appendToNalUnit(dataArray, offset, startCodeOffset); + } else { + bytesAlreadyPassed = -lengthToStartCode; + } + if (userData.endNalUnit(bytesAlreadyPassed)) { + int unescapedLength = NalUnitUtil.unescapeStream(userData.nalData, userData.nalLength); + userDataParsable.reset(userData.nalData, unescapedLength); + userDataReader.consume(sampleTimeUs, userDataParsable); + } + + if (startCodeValue == START_USER_DATA && data.data[startCodeOffset + 2] == 0x1) { + userData.startNalUnit(startCodeValue); + } + } if (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER) { int bytesWrittenPastStartCode = limit - startCodeOffset; if (startedFirstSample && sampleHasPicture && hasOutputFormat) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java index 3cde946ce3..45e094f69d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.NalUnitUtil.SpsData; @@ -180,9 +181,23 @@ public final class H264Reader implements ElementaryStreamReader { initializationData.add(Arrays.copyOf(pps.nalData, pps.nalLength)); NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength); NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength); - output.format(Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H264, null, - Format.NO_VALUE, Format.NO_VALUE, spsData.width, spsData.height, Format.NO_VALUE, - initializationData, Format.NO_VALUE, spsData.pixelWidthAspectRatio, null)); + output.format( + Format.createVideoSampleFormat( + formatId, + MimeTypes.VIDEO_H264, + CodecSpecificDataUtil.buildAvcCodecString( + spsData.profileIdc, + spsData.constraintsFlagsAndReservedZero2Bits, + spsData.levelIdc), + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + spsData.width, + spsData.height, + /* frameRate= */ Format.NO_VALUE, + initializationData, + /* rotationDegrees= */ Format.NO_VALUE, + spsData.pixelWidthAspectRatio, + /* drmInitData= */ null)); hasOutputFormat = true; sampleReader.putSps(spsData); sampleReader.putPps(ppsData); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java new file mode 100644 index 0000000000..e8c207f75d --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.ts; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.BinarySearchSeeker; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; +import java.io.IOException; + +/** + * A seeker that supports seeking within PS stream using binary search. + * + *

    This seeker uses the first and last SCR values within the stream, as well as the stream + * duration to interpolate the SCR value of the seeking position. Then it performs binary search + * within the stream to find a packets whose SCR value is with in {@link #SEEK_TOLERANCE_US} from + * the target SCR. + */ +/* package */ final class PsBinarySearchSeeker extends BinarySearchSeeker { + + private static final long SEEK_TOLERANCE_US = 100_000; + private static final int MINIMUM_SEARCH_RANGE_BYTES = 1000; + private static final int TIMESTAMP_SEARCH_BYTES = 20000; + + public PsBinarySearchSeeker( + TimestampAdjuster scrTimestampAdjuster, long streamDurationUs, long inputLength) { + super( + new DefaultSeekTimestampConverter(), + new PsScrSeeker(scrTimestampAdjuster), + streamDurationUs, + /* floorTimePosition= */ 0, + /* ceilingTimePosition= */ streamDurationUs + 1, + /* floorBytePosition= */ 0, + /* ceilingBytePosition= */ inputLength, + /* approxBytesPerFrame= */ TsExtractor.TS_PACKET_SIZE, + MINIMUM_SEARCH_RANGE_BYTES); + } + + /** + * A seeker that looks for a given SCR timestamp at a given position in a PS stream. + * + *

    Given a SCR timestamp, and a position within a PS stream, this seeker will try to read a + * range of up to {@link #TIMESTAMP_SEARCH_BYTES} bytes from that stream position, look for all + * packs in that range, and then compare the SCR timestamps (if available) of these packets vs the + * target timestamp. + */ + private static final class PsScrSeeker implements TimestampSeeker { + + private final TimestampAdjuster scrTimestampAdjuster; + private final ParsableByteArray packetBuffer; + + private PsScrSeeker(TimestampAdjuster scrTimestampAdjuster) { + this.scrTimestampAdjuster = scrTimestampAdjuster; + packetBuffer = new ParsableByteArray(TIMESTAMP_SEARCH_BYTES); + } + + @Override + public TimestampSearchResult searchForTimestamp( + ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder) + throws IOException, InterruptedException { + long inputPosition = input.getPosition(); + int bytesToRead = + (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - input.getPosition()); + packetBuffer.reset(bytesToRead); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead); + + return searchForScrValueInBuffer(packetBuffer, targetTimestamp, inputPosition); + } + + private TimestampSearchResult searchForScrValueInBuffer( + ParsableByteArray packetBuffer, long targetScrTimeUs, long bufferStartOffset) { + int startOfLastPacketPosition = C.POSITION_UNSET; + int endOfLastPacketPosition = C.POSITION_UNSET; + long lastScrTimeUsInRange = C.TIME_UNSET; + + while (packetBuffer.bytesLeft() >= 4) { + int nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition()); + if (nextStartCode != PsExtractor.PACK_START_CODE) { + packetBuffer.skipBytes(1); + continue; + } else { + packetBuffer.skipBytes(4); + } + + // We found a pack. + long scrValue = PsDurationReader.readScrValueFromPack(packetBuffer); + if (scrValue != C.TIME_UNSET) { + long scrTimeUs = scrTimestampAdjuster.adjustTsTimestamp(scrValue); + if (scrTimeUs > targetScrTimeUs) { + if (lastScrTimeUsInRange == C.TIME_UNSET) { + // First SCR timestamp is already over target. + return TimestampSearchResult.overestimatedResult(scrTimeUs, bufferStartOffset); + } else { + // Last SCR timestamp < target timestamp < this timestamp. + return TimestampSearchResult.targetFoundResult( + bufferStartOffset + startOfLastPacketPosition); + } + } else if (scrTimeUs + SEEK_TOLERANCE_US > targetScrTimeUs) { + long startOfPacketInStream = bufferStartOffset + packetBuffer.getPosition(); + return TimestampSearchResult.targetFoundResult(startOfPacketInStream); + } + + lastScrTimeUsInRange = scrTimeUs; + startOfLastPacketPosition = packetBuffer.getPosition(); + } + skipToEndOfCurrentPack(packetBuffer); + endOfLastPacketPosition = packetBuffer.getPosition(); + } + + if (lastScrTimeUsInRange != C.TIME_UNSET) { + long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition; + return TimestampSearchResult.underestimatedResult( + lastScrTimeUsInRange, endOfLastPacketPositionInStream); + } else { + return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT; + } + } + + /** + * Skips the buffer position to the position after the end of the current PS pack in the buffer, + * given the byte position right after the {@link PsExtractor#PACK_START_CODE} of the pack in + * the buffer. If the pack ends after the end of the buffer, skips to the end of the buffer. + */ + private static void skipToEndOfCurrentPack(ParsableByteArray packetBuffer) { + int limit = packetBuffer.limit(); + + if (packetBuffer.bytesLeft() < 10) { + // We require at least 9 bytes for pack header to read SCR value + 1 byte for pack_stuffing + // length. + packetBuffer.setPosition(limit); + return; + } + packetBuffer.skipBytes(9); + + int packStuffingLength = packetBuffer.readUnsignedByte() & 0x07; + if (packetBuffer.bytesLeft() < packStuffingLength) { + packetBuffer.setPosition(limit); + return; + } + packetBuffer.skipBytes(packStuffingLength); + + if (packetBuffer.bytesLeft() < 4) { + packetBuffer.setPosition(limit); + return; + } + + int nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition()); + if (nextStartCode == PsExtractor.SYSTEM_HEADER_START_CODE) { + packetBuffer.skipBytes(4); + int systemHeaderLength = packetBuffer.readUnsignedShort(); + if (packetBuffer.bytesLeft() < systemHeaderLength) { + packetBuffer.setPosition(limit); + return; + } + packetBuffer.skipBytes(systemHeaderLength); + } + + // Find the position of the next PACK_START_CODE or MPEG_PROGRAM_END_CODE, which is right + // after the end position of this pack. + // If we couldn't find these codes within the buffer, return the buffer limit, or return + // the first position which PES packets pattern does not match (some malformed packets). + while (packetBuffer.bytesLeft() >= 4) { + nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition()); + if (nextStartCode == PsExtractor.PACK_START_CODE + || nextStartCode == PsExtractor.MPEG_PROGRAM_END_CODE) { + break; + } + if (nextStartCode >>> 8 != PsExtractor.PACKET_START_CODE_PREFIX) { + break; + } + packetBuffer.skipBytes(4); + + if (packetBuffer.bytesLeft() < 2) { + // 2 bytes for PES_packet length. + packetBuffer.setPosition(limit); + return; + } + int pesPacketLength = packetBuffer.readUnsignedShort(); + packetBuffer.setPosition( + Math.min(packetBuffer.limit(), packetBuffer.getPosition() + pesPacketLength)); + } + } + } + + private static int peekIntAtPosition(byte[] data, int position) { + return (data[position] & 0xFF) << 24 + | (data[position + 1] & 0xFF) << 16 + | (data[position + 2] & 0xFF) << 8 + | (data[position + 3] & 0xFF); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java new file mode 100644 index 0000000000..3b52206235 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.ts; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; +import java.io.IOException; + +/** + * A reader that can extract the approximate duration from a given MPEG program stream (PS). + * + *

    This reader extracts the duration by reading system clock reference (SCR) values from the + * header of a pack at the start and at the end of the stream, calculating the difference, and + * converting that into stream duration. This reader also handles the case when a single SCR + * wraparound takes place within the stream, which can make SCR values at the beginning of the + * stream larger than SCR values at the end. This class can only be used once to read duration from + * a given stream, and the usage of the class is not thread-safe, so all calls should be made from + * the same thread. + * + *

    Note: See ISO/IEC 13818-1, Table 2-33 for details of the SCR field in pack_header. + */ +/* package */ final class PsDurationReader { + + private static final int DURATION_READ_BYTES = 20000; + + private final TimestampAdjuster scrTimestampAdjuster; + private final ParsableByteArray packetBuffer; + + private boolean isDurationRead; + private boolean isFirstScrValueRead; + private boolean isLastScrValueRead; + + private long firstScrValue; + private long lastScrValue; + private long durationUs; + + /* package */ PsDurationReader() { + scrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); + firstScrValue = C.TIME_UNSET; + lastScrValue = C.TIME_UNSET; + durationUs = C.TIME_UNSET; + packetBuffer = new ParsableByteArray(DURATION_READ_BYTES); + } + + /** Returns true if a PS duration has been read. */ + public boolean isDurationReadFinished() { + return isDurationRead; + } + + public TimestampAdjuster getScrTimestampAdjuster() { + return scrTimestampAdjuster; + } + + /** + * Reads a PS duration from the input. + * + *

    This reader reads the duration by reading SCR values from the header of a pack at the start + * and at the end of the stream, calculating the difference, and converting that into stream + * duration. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated + * to hold the position of the required seek. + * @return One of the {@code RESULT_} values defined in {@link Extractor}. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + public @Extractor.ReadResult int readDuration( + ExtractorInput input, PositionHolder seekPositionHolder) + throws IOException, InterruptedException { + if (!isLastScrValueRead) { + return readLastScrValue(input, seekPositionHolder); + } + if (lastScrValue == C.TIME_UNSET) { + return finishReadDuration(input); + } + if (!isFirstScrValueRead) { + return readFirstScrValue(input, seekPositionHolder); + } + if (firstScrValue == C.TIME_UNSET) { + return finishReadDuration(input); + } + + long minScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(firstScrValue); + long maxScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(lastScrValue); + durationUs = maxScrPositionUs - minScrPositionUs; + return finishReadDuration(input); + } + + /** Returns the duration last read from {@link #readDuration(ExtractorInput, PositionHolder)}. */ + public long getDurationUs() { + return durationUs; + } + + /** + * Returns the SCR value read from the next pack in the stream, given the buffer at the pack + * header start position (just behind the pack start code). + */ + public static long readScrValueFromPack(ParsableByteArray packetBuffer) { + int originalPosition = packetBuffer.getPosition(); + if (packetBuffer.bytesLeft() < 9) { + // We require at 9 bytes for pack header to read scr value + return C.TIME_UNSET; + } + byte[] scrBytes = new byte[9]; + packetBuffer.readBytes(scrBytes, /* offset= */ 0, scrBytes.length); + packetBuffer.setPosition(originalPosition); + if (!checkMarkerBits(scrBytes)) { + return C.TIME_UNSET; + } + return readScrValueFromPackHeader(scrBytes); + } + + private int finishReadDuration(ExtractorInput input) { + isDurationRead = true; + input.resetPeekPosition(); + return Extractor.RESULT_CONTINUE; + } + + private int readFirstScrValue(ExtractorInput input, PositionHolder seekPositionHolder) + throws IOException, InterruptedException { + if (input.getPosition() != 0) { + seekPositionHolder.position = 0; + return Extractor.RESULT_SEEK; + } + + int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength()); + input.resetPeekPosition(); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead); + packetBuffer.setPosition(0); + packetBuffer.setLimit(bytesToRead); + + firstScrValue = readFirstScrValueFromBuffer(packetBuffer); + isFirstScrValueRead = true; + return Extractor.RESULT_CONTINUE; + } + + private long readFirstScrValueFromBuffer(ParsableByteArray packetBuffer) { + int searchStartPosition = packetBuffer.getPosition(); + int searchEndPosition = packetBuffer.limit(); + for (int searchPosition = searchStartPosition; + searchPosition < searchEndPosition - 3; + searchPosition++) { + int nextStartCode = peekIntAtPosition(packetBuffer.data, searchPosition); + if (nextStartCode == PsExtractor.PACK_START_CODE) { + packetBuffer.setPosition(searchPosition + 4); + long scrValue = readScrValueFromPack(packetBuffer); + if (scrValue != C.TIME_UNSET) { + return scrValue; + } + } + } + return C.TIME_UNSET; + } + + private int readLastScrValue(ExtractorInput input, PositionHolder seekPositionHolder) + throws IOException, InterruptedException { + int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength()); + long bufferStartStreamPosition = input.getLength() - bytesToRead; + if (input.getPosition() != bufferStartStreamPosition) { + seekPositionHolder.position = bufferStartStreamPosition; + return Extractor.RESULT_SEEK; + } + + input.resetPeekPosition(); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead); + packetBuffer.setPosition(0); + packetBuffer.setLimit(bytesToRead); + + lastScrValue = readLastScrValueFromBuffer(packetBuffer); + isLastScrValueRead = true; + return Extractor.RESULT_CONTINUE; + } + + private long readLastScrValueFromBuffer(ParsableByteArray packetBuffer) { + int searchStartPosition = packetBuffer.getPosition(); + int searchEndPosition = packetBuffer.limit(); + for (int searchPosition = searchEndPosition - 4; + searchPosition >= searchStartPosition; + searchPosition--) { + int nextStartCode = peekIntAtPosition(packetBuffer.data, searchPosition); + if (nextStartCode == PsExtractor.PACK_START_CODE) { + packetBuffer.setPosition(searchPosition + 4); + long scrValue = readScrValueFromPack(packetBuffer); + if (scrValue != C.TIME_UNSET) { + return scrValue; + } + } + } + return C.TIME_UNSET; + } + + private int peekIntAtPosition(byte[] data, int position) { + return (data[position] & 0xFF) << 24 + | (data[position + 1] & 0xFF) << 16 + | (data[position + 2] & 0xFF) << 8 + | (data[position + 3] & 0xFF); + } + + private static boolean checkMarkerBits(byte[] scrBytes) { + // Verify the 01xxx1xx marker on the 0th byte + if ((scrBytes[0] & 0xC4) != 0x44) { + return false; + } + // 1st byte belongs to scr field. + // Verify the xxxxx1xx marker on the 2nd byte + if ((scrBytes[2] & 0x04) != 0x04) { + return false; + } + // 3rd byte belongs to scr field. + // Verify the xxxxx1xx marker on the 4rd byte + if ((scrBytes[4] & 0x04) != 0x04) { + return false; + } + // Verify the xxxxxxx1 marker on the 5th byte + if ((scrBytes[5] & 0x01) != 0x01) { + return false; + } + // 6th and 7th bytes belongs to program_max_rate field. + // Verify the xxxxxx11 marker on the 8th byte + return (scrBytes[8] & 0x03) == 0x03; + } + + /** + * Returns the value of SCR base - 33 bits in big endian order from the PS pack header, ignoring + * the marker bits. Note: See ISO/IEC 13818-1, Table 2-33 for details of the SCR field in + * pack_header. + * + *

    We ignore SCR Ext, because it's too small to have any significance. + */ + private static long readScrValueFromPackHeader(byte[] scrBytes) { + return ((scrBytes[0] & 0b00111000L) >> 3) << 30 + | (scrBytes[0] & 0b00000011L) << 28 + | (scrBytes[1] & 0xFFL) << 20 + | ((scrBytes[2] & 0b11111000L) >> 3) << 15 + | (scrBytes[2] & 0b00000011L) << 13 + | (scrBytes[3] & 0xFFL) << 5 + | (scrBytes[4] & 0b11111000L) >> 3; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java index f3aad6ba6b..c7a082aeac 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -35,24 +35,20 @@ import java.io.IOException; */ public final class PsExtractor implements Extractor { - /** - * Factory for {@link PsExtractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { + /** Factory for {@link PsExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new PsExtractor()}; - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new PsExtractor()}; - } - - }; - - private static final int PACK_START_CODE = 0x000001BA; - private static final int SYSTEM_HEADER_START_CODE = 0x000001BB; - private static final int PACKET_START_CODE_PREFIX = 0x000001; - private static final int MPEG_PROGRAM_END_CODE = 0x000001B9; + /* package */ static final int PACK_START_CODE = 0x000001BA; + /* package */ static final int SYSTEM_HEADER_START_CODE = 0x000001BB; + /* package */ static final int PACKET_START_CODE_PREFIX = 0x000001; + /* package */ static final int MPEG_PROGRAM_END_CODE = 0x000001B9; private static final int MAX_STREAM_ID_PLUS_ONE = 0x100; + + // Max search length for first audio and video track in input data. private static final long MAX_SEARCH_LENGTH = 1024 * 1024; + // Max search length for additional audio and video tracks in input data after at least one audio + // and video track has been found. + private static final long MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND = 8 * 1024; public static final int PRIVATE_STREAM_1 = 0xBD; public static final int AUDIO_STREAM = 0xC0; @@ -63,12 +59,17 @@ public final class PsExtractor implements Extractor { private final TimestampAdjuster timestampAdjuster; private final SparseArray psPayloadReaders; // Indexed by pid private final ParsableByteArray psPacketBuffer; + private final PsDurationReader durationReader; + private boolean foundAllTracks; private boolean foundAudioTrack; private boolean foundVideoTrack; + private long lastTrackPosition; // Accessed only by the loading thread. + private PsBinarySearchSeeker psBinarySearchSeeker; private ExtractorOutput output; + private boolean hasOutputSeekMap; public PsExtractor() { this(new TimestampAdjuster(0)); @@ -78,6 +79,7 @@ public final class PsExtractor implements Extractor { this.timestampAdjuster = timestampAdjuster; psPacketBuffer = new ParsableByteArray(4096); psPayloadReaders = new SparseArray<>(); + durationReader = new PsDurationReader(); } // Extractor implementation. @@ -124,12 +126,27 @@ public final class PsExtractor implements Extractor { @Override public void init(ExtractorOutput output) { this.output = output; - output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); } @Override public void seek(long position, long timeUs) { - timestampAdjuster.reset(); + boolean hasNotEncounteredFirstTimestamp = + timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET; + if (hasNotEncounteredFirstTimestamp + || (timestampAdjuster.getFirstSampleTimestampUs() != 0 + && timestampAdjuster.getFirstSampleTimestampUs() != timeUs)) { + // - If the timestamp adjuster in the PS stream has not encountered any sample, it's going to + // treat the first timestamp encountered as sample time 0, which is incorrect. In this case, + // we have to set the first sample timestamp manually. + // - If the timestamp adjuster has its timestamp set manually before, and now we seek to a + // different position, we need to set the first sample timestamp manually again. + timestampAdjuster.reset(); + timestampAdjuster.setFirstSampleTimestampUs(timeUs); + } + + if (psBinarySearchSeeker != null) { + psBinarySearchSeeker.setSeekTargetUs(timeUs); + } for (int i = 0; i < psPayloadReaders.size(); i++) { psPayloadReaders.valueAt(i).seek(); } @@ -143,6 +160,24 @@ public final class PsExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + + long inputLength = input.getLength(); + boolean canReadDuration = inputLength != C.LENGTH_UNSET; + if (canReadDuration && !durationReader.isDurationReadFinished()) { + return durationReader.readDuration(input, seekPosition); + } + maybeOutputSeekMap(inputLength); + if (psBinarySearchSeeker != null && psBinarySearchSeeker.isSeeking()) { + return psBinarySearchSeeker.handlePendingSeek( + input, seekPosition, /* outputFrameHolder= */ null); + } + + input.resetPeekPosition(); + long peekBytesLeft = + inputLength != C.LENGTH_UNSET ? inputLength - input.getPeekPosition() : C.LENGTH_UNSET; + if (peekBytesLeft != C.LENGTH_UNSET && peekBytesLeft < 4) { + return RESULT_END_OF_INPUT; + } // First peek and check what type of start code is next. if (!input.peekFully(psPacketBuffer.data, 0, 4, true)) { return RESULT_END_OF_INPUT; @@ -188,18 +223,21 @@ public final class PsExtractor implements Extractor { if (!foundAllTracks) { if (payloadReader == null) { ElementaryStreamReader elementaryStreamReader = null; - if (!foundAudioTrack && streamId == PRIVATE_STREAM_1) { + if (streamId == PRIVATE_STREAM_1) { // Private stream, used for AC3 audio. // NOTE: This may need further parsing to determine if its DTS, but that's likely only // valid for DVDs. elementaryStreamReader = new Ac3Reader(); foundAudioTrack = true; - } else if (!foundAudioTrack && (streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) { + lastTrackPosition = input.getPosition(); + } else if ((streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) { elementaryStreamReader = new MpegAudioReader(); foundAudioTrack = true; - } else if (!foundVideoTrack && (streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) { + lastTrackPosition = input.getPosition(); + } else if ((streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) { elementaryStreamReader = new H262Reader(); foundVideoTrack = true; + lastTrackPosition = input.getPosition(); } if (elementaryStreamReader != null) { TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE); @@ -208,7 +246,11 @@ public final class PsExtractor implements Extractor { psPayloadReaders.put(streamId, payloadReader); } } - if ((foundAudioTrack && foundVideoTrack) || input.getPosition() > MAX_SEARCH_LENGTH) { + long maxSearchPosition = + foundAudioTrack && foundVideoTrack + ? lastTrackPosition + MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND + : MAX_SEARCH_LENGTH; + if (input.getPosition() > maxSearchPosition) { foundAllTracks = true; output.endTracks(); } @@ -237,6 +279,22 @@ public final class PsExtractor implements Extractor { // Internals. + private void maybeOutputSeekMap(long inputLength) { + if (!hasOutputSeekMap) { + hasOutputSeekMap = true; + if (durationReader.getDurationUs() != C.TIME_UNSET) { + psBinarySearchSeeker = + new PsBinarySearchSeeker( + durationReader.getScrTimestampAdjuster(), + durationReader.getDurationUs(), + inputLength); + output.seekMap(psBinarySearchSeeker.getSeekMap()); + } else { + output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs())); + } + } + } + /** * Parses PES packet data and extracts samples. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java index 907419f8fc..895c224697 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -52,9 +52,18 @@ import java.util.List; || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), "Invalid closed caption mime type provided: " + channelMimeType); String formatId = channelFormat.id != null ? channelFormat.id : idGenerator.getFormatId(); - output.format(Format.createTextSampleFormat(formatId, channelMimeType, null, Format.NO_VALUE, - channelFormat.selectionFlags, channelFormat.language, channelFormat.accessibilityChannel, - null)); + output.format( + Format.createTextSampleFormat( + formatId, + channelMimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + channelFormat.selectionFlags, + channelFormat.language, + channelFormat.accessibilityChannel, + /* drmInitData= */ null, + Format.OFFSET_SAMPLE_RELATIVE, + channelFormat.initializationData)); outputs[i] = output; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java new file mode 100644 index 0000000000..29aa0d55d2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.ts; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.BinarySearchSeeker; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; +import java.io.IOException; + +/** + * A seeker that supports seeking within TS stream using binary search. + * + *

    This seeker uses the first and last PCR values within the stream, as well as the stream + * duration to interpolate the PCR value of the seeking position. Then it performs binary search + * within the stream to find a packets whose PCR value is within {@link #SEEK_TOLERANCE_US} from the + * target PCR. + */ +/* package */ final class TsBinarySearchSeeker extends BinarySearchSeeker { + + private static final long SEEK_TOLERANCE_US = 100_000; + private static final int MINIMUM_SEARCH_RANGE_BYTES = TsExtractor.TS_PACKET_SIZE * 5; + private static final int TIMESTAMP_SEARCH_PACKETS = 200; + private static final int TIMESTAMP_SEARCH_BYTES = + TsExtractor.TS_PACKET_SIZE * TIMESTAMP_SEARCH_PACKETS; + + public TsBinarySearchSeeker( + TimestampAdjuster pcrTimestampAdjuster, long streamDurationUs, long inputLength, int pcrPid) { + super( + new DefaultSeekTimestampConverter(), + new TsPcrSeeker(pcrPid, pcrTimestampAdjuster), + streamDurationUs, + /* floorTimePosition= */ 0, + /* ceilingTimePosition= */ streamDurationUs + 1, + /* floorBytePosition= */ 0, + /* ceilingBytePosition= */ inputLength, + /* approxBytesPerFrame= */ TsExtractor.TS_PACKET_SIZE, + MINIMUM_SEARCH_RANGE_BYTES); + } + + /** + * A {@link TimestampSeeker} implementation that looks for a given PCR timestamp at a given + * position in a TS stream. + * + *

    Given a PCR timestamp, and a position within a TS stream, this seeker will try to read up to + * {@link #TIMESTAMP_SEARCH_PACKETS} TS packets from that stream position, look for all packet + * with PID equals to PCR_PID, and then compare the PCR timestamps (if available) of these packets + * vs the target timestamp. + */ + private static final class TsPcrSeeker implements TimestampSeeker { + + private final TimestampAdjuster pcrTimestampAdjuster; + private final ParsableByteArray packetBuffer; + private final int pcrPid; + + public TsPcrSeeker(int pcrPid, TimestampAdjuster pcrTimestampAdjuster) { + this.pcrPid = pcrPid; + this.pcrTimestampAdjuster = pcrTimestampAdjuster; + packetBuffer = new ParsableByteArray(TIMESTAMP_SEARCH_BYTES); + } + + @Override + public TimestampSearchResult searchForTimestamp( + ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder) + throws IOException, InterruptedException { + long inputPosition = input.getPosition(); + int bytesToRead = + (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - input.getPosition()); + packetBuffer.reset(bytesToRead); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead); + + return searchForPcrValueInBuffer(packetBuffer, targetTimestamp, inputPosition); + } + + private TimestampSearchResult searchForPcrValueInBuffer( + ParsableByteArray packetBuffer, long targetPcrTimeUs, long bufferStartOffset) { + int limit = packetBuffer.limit(); + + long startOfLastPacketPosition = C.POSITION_UNSET; + long endOfLastPacketPosition = C.POSITION_UNSET; + long lastPcrTimeUsInRange = C.TIME_UNSET; + + while (packetBuffer.bytesLeft() >= TsExtractor.TS_PACKET_SIZE) { + int startOfPacket = + TsUtil.findSyncBytePosition(packetBuffer.data, packetBuffer.getPosition(), limit); + int endOfPacket = startOfPacket + TsExtractor.TS_PACKET_SIZE; + if (endOfPacket > limit) { + break; + } + long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, startOfPacket, pcrPid); + if (pcrValue != C.TIME_UNSET) { + long pcrTimeUs = pcrTimestampAdjuster.adjustTsTimestamp(pcrValue); + if (pcrTimeUs > targetPcrTimeUs) { + if (lastPcrTimeUsInRange == C.TIME_UNSET) { + // First PCR timestamp is already over target. + return TimestampSearchResult.overestimatedResult(pcrTimeUs, bufferStartOffset); + } else { + // Last PCR timestamp < target timestamp < this timestamp. + return TimestampSearchResult.targetFoundResult( + bufferStartOffset + startOfLastPacketPosition); + } + } else if (pcrTimeUs + SEEK_TOLERANCE_US > targetPcrTimeUs) { + long startOfPacketInStream = bufferStartOffset + startOfPacket; + return TimestampSearchResult.targetFoundResult(startOfPacketInStream); + } + + lastPcrTimeUsInRange = pcrTimeUs; + startOfLastPacketPosition = startOfPacket; + } + packetBuffer.setPosition(endOfPacket); + endOfLastPacketPosition = endOfPacket; + } + + if (lastPcrTimeUsInRange != C.TIME_UNSET) { + long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition; + return TimestampSearchResult.underestimatedResult( + lastPcrTimeUsInRange, endOfLastPacketPositionInStream); + } else { + return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT; + } + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java new file mode 100644 index 0000000000..350337cc86 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.ts; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; +import java.io.IOException; + +/** + * A reader that can extract the approximate duration from a given MPEG transport stream (TS). + * + *

    This reader extracts the duration by reading PCR values of the PCR PID packets at the start + * and at the end of the stream, calculating the difference, and converting that into stream + * duration. This reader also handles the case when a single PCR wraparound takes place within the + * stream, which can make PCR values at the beginning of the stream larger than PCR values at the + * end. This class can only be used once to read duration from a given stream, and the usage of the + * class is not thread-safe, so all calls should be made from the same thread. + */ +/* package */ final class TsDurationReader { + + private static final int DURATION_READ_PACKETS = 200; + private static final int DURATION_READ_BYTES = TsExtractor.TS_PACKET_SIZE * DURATION_READ_PACKETS; + + private final TimestampAdjuster pcrTimestampAdjuster; + private final ParsableByteArray packetBuffer; + + private boolean isDurationRead; + private boolean isFirstPcrValueRead; + private boolean isLastPcrValueRead; + + private long firstPcrValue; + private long lastPcrValue; + private long durationUs; + + /* package */ TsDurationReader() { + pcrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); + firstPcrValue = C.TIME_UNSET; + lastPcrValue = C.TIME_UNSET; + durationUs = C.TIME_UNSET; + packetBuffer = new ParsableByteArray(DURATION_READ_BYTES); + } + + /** Returns true if a TS duration has been read. */ + public boolean isDurationReadFinished() { + return isDurationRead; + } + + /** + * Reads a TS duration from the input, using the given PCR PID. + * + *

    This reader reads the duration by reading PCR values of the PCR PID packets at the start and + * at the end of the stream, calculating the difference, and converting that into stream duration. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated + * to hold the position of the required seek. + * @param pcrPid The PID of the packet stream within this TS stream that contains PCR values. + * @return One of the {@code RESULT_} values defined in {@link Extractor}. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + public @Extractor.ReadResult int readDuration( + ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) + throws IOException, InterruptedException { + if (pcrPid <= 0) { + return finishReadDuration(input); + } + if (!isLastPcrValueRead) { + return readLastPcrValue(input, seekPositionHolder, pcrPid); + } + if (lastPcrValue == C.TIME_UNSET) { + return finishReadDuration(input); + } + if (!isFirstPcrValueRead) { + return readFirstPcrValue(input, seekPositionHolder, pcrPid); + } + if (firstPcrValue == C.TIME_UNSET) { + return finishReadDuration(input); + } + + long minPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(firstPcrValue); + long maxPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(lastPcrValue); + durationUs = maxPcrPositionUs - minPcrPositionUs; + return finishReadDuration(input); + } + + /** + * Returns the duration last read from {@link #readDuration(ExtractorInput, PositionHolder, int)}. + */ + public long getDurationUs() { + return durationUs; + } + + /** + * Returns the {@link TimestampAdjuster} that this class uses to adjust timestamps read from the + * input TS stream. + */ + public TimestampAdjuster getPcrTimestampAdjuster() { + return pcrTimestampAdjuster; + } + + private int finishReadDuration(ExtractorInput input) { + isDurationRead = true; + input.resetPeekPosition(); + return Extractor.RESULT_CONTINUE; + } + + private int readFirstPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) + throws IOException, InterruptedException { + if (input.getPosition() != 0) { + seekPositionHolder.position = 0; + return Extractor.RESULT_SEEK; + } + + int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength()); + input.resetPeekPosition(); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead); + packetBuffer.setPosition(0); + packetBuffer.setLimit(bytesToRead); + + firstPcrValue = readFirstPcrValueFromBuffer(packetBuffer, pcrPid); + isFirstPcrValueRead = true; + return Extractor.RESULT_CONTINUE; + } + + private long readFirstPcrValueFromBuffer(ParsableByteArray packetBuffer, int pcrPid) { + int searchStartPosition = packetBuffer.getPosition(); + int searchEndPosition = packetBuffer.limit(); + for (int searchPosition = searchStartPosition; + searchPosition < searchEndPosition; + searchPosition++) { + if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) { + continue; + } + long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, searchPosition, pcrPid); + if (pcrValue != C.TIME_UNSET) { + return pcrValue; + } + } + return C.TIME_UNSET; + } + + private int readLastPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) + throws IOException, InterruptedException { + int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength()); + long bufferStartStreamPosition = input.getLength() - bytesToRead; + if (input.getPosition() != bufferStartStreamPosition) { + seekPositionHolder.position = bufferStartStreamPosition; + return Extractor.RESULT_SEEK; + } + + input.resetPeekPosition(); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead); + packetBuffer.setPosition(0); + packetBuffer.setLimit(bytesToRead); + + lastPcrValue = readLastPcrValueFromBuffer(packetBuffer, pcrPid); + isLastPcrValueRead = true; + return Extractor.RESULT_CONTINUE; + } + + private long readLastPcrValueFromBuffer(ParsableByteArray packetBuffer, int pcrPid) { + int searchStartPosition = packetBuffer.getPosition(); + int searchEndPosition = packetBuffer.limit(); + for (int searchPosition = searchEndPosition - 1; + searchPosition >= searchStartPosition; + searchPosition--) { + if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) { + continue; + } + long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, searchPosition, pcrPid); + if (pcrValue != C.TIME_UNSET) { + return pcrValue; + } + } + return C.TIME_UNSET; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 50931e2d90..f677dc008f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -50,17 +50,8 @@ import java.util.List; */ public final class TsExtractor implements Extractor { - /** - * Factory for {@link TsExtractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { - - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new TsExtractor()}; - } - - }; + /** Factory for {@link TsExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new TsExtractor()}; /** * Modes for the extractor. @@ -98,8 +89,9 @@ public final class TsExtractor implements Extractor { public static final int TS_STREAM_TYPE_SPLICE_INFO = 0x86; public static final int TS_STREAM_TYPE_DVBSUBS = 0x59; - private static final int TS_PACKET_SIZE = 188; - private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. + public static final int TS_PACKET_SIZE = 188; + public static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. + private static final int TS_PAT_PID = 0; private static final int MAX_PID_PLUS_ONE = 0x2000; @@ -110,20 +102,26 @@ public final class TsExtractor implements Extractor { private static final int BUFFER_SIZE = TS_PACKET_SIZE * 50; private static final int SNIFF_TS_PACKET_COUNT = 5; - @Mode private final int mode; + private final @Mode int mode; private final List timestampAdjusters; private final ParsableByteArray tsPacketBuffer; private final SparseIntArray continuityCounters; private final TsPayloadReader.Factory payloadReaderFactory; private final SparseArray tsPayloadReaders; // Indexed by pid private final SparseBooleanArray trackIds; + private final SparseBooleanArray trackPids; + private final TsDurationReader durationReader; // Accessed only by the loading thread. + private TsBinarySearchSeeker tsBinarySearchSeeker; private ExtractorOutput output; private int remainingPmts; private boolean tracksEnded; + private boolean hasOutputSeekMap; + private boolean pendingSeekToStart; private TsPayloadReader id3Reader; private int bytesSinceLastSync; + private int pcrPid; public TsExtractor() { this(0); @@ -144,18 +142,21 @@ public final class TsExtractor implements Extractor { * {@code FLAG_*} values that control the behavior of the payload readers. */ public TsExtractor(@Mode int mode, @Flags int defaultTsPayloadReaderFlags) { - this(mode, new TimestampAdjuster(0), + this( + mode, + new TimestampAdjuster(0), new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags)); } - /** * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} * and {@link #MODE_HLS}. * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. * @param payloadReaderFactory Factory for injecting a custom set of payload readers. */ - public TsExtractor(@Mode int mode, TimestampAdjuster timestampAdjuster, + public TsExtractor( + @Mode int mode, + TimestampAdjuster timestampAdjuster, TsPayloadReader.Factory payloadReaderFactory) { this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory); this.mode = mode; @@ -167,8 +168,11 @@ public final class TsExtractor implements Extractor { } tsPacketBuffer = new ParsableByteArray(new byte[BUFFER_SIZE], 0); trackIds = new SparseBooleanArray(); + trackPids = new SparseBooleanArray(); tsPayloadReaders = new SparseArray<>(); continuityCounters = new SparseIntArray(); + durationReader = new TsDurationReader(); + pcrPid = -1; resetPayloadReaders(); } @@ -178,16 +182,19 @@ public final class TsExtractor implements Extractor { public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { byte[] buffer = tsPacketBuffer.data; input.peekFully(buffer, 0, TS_PACKET_SIZE * SNIFF_TS_PACKET_COUNT); - for (int j = 0; j < TS_PACKET_SIZE; j++) { - for (int i = 0; true; i++) { - if (i == SNIFF_TS_PACKET_COUNT) { - input.skipFully(j); - return true; - } - if (buffer[j + i * TS_PACKET_SIZE] != TS_SYNC_BYTE) { + for (int startPosCandidate = 0; startPosCandidate < TS_PACKET_SIZE; startPosCandidate++) { + // Try to identify at least SNIFF_TS_PACKET_COUNT packets starting with TS_SYNC_BYTE. + boolean isSyncBytePatternCorrect = true; + for (int i = 0; i < SNIFF_TS_PACKET_COUNT; i++) { + if (buffer[startPosCandidate + i * TS_PACKET_SIZE] != TS_SYNC_BYTE) { + isSyncBytePatternCorrect = false; break; } } + if (isSyncBytePatternCorrect) { + input.skipFully(startPosCandidate); + return true; + } } return false; } @@ -195,19 +202,36 @@ public final class TsExtractor implements Extractor { @Override public void init(ExtractorOutput output) { this.output = output; - output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); } @Override public void seek(long position, long timeUs) { + Assertions.checkState(mode != MODE_HLS); int timestampAdjustersCount = timestampAdjusters.size(); for (int i = 0; i < timestampAdjustersCount; i++) { - timestampAdjusters.get(i).reset(); + TimestampAdjuster timestampAdjuster = timestampAdjusters.get(i); + boolean hasNotEncounteredFirstTimestamp = + timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET; + if (hasNotEncounteredFirstTimestamp + || (timestampAdjuster.getTimestampOffsetUs() != 0 + && timestampAdjuster.getFirstSampleTimestampUs() != timeUs)) { + // - If a track in the TS stream has not encountered any sample, it's going to treat the + // first sample encountered as timestamp 0, which is incorrect. So we have to set the first + // sample timestamp for that track manually. + // - If the timestamp adjuster has its timestamp set manually before, and now we seek to a + // different position, we need to set the first sample timestamp manually again. + timestampAdjuster.reset(); + timestampAdjuster.setFirstSampleTimestampUs(timeUs); + } + } + if (timeUs != 0 && tsBinarySearchSeeker != null) { + tsBinarySearchSeeker.setSeekTargetUs(timeUs); } tsPacketBuffer.reset(); continuityCounters.clear(); - // Elementary stream readers' state should be cleared to get consistent behaviours when seeking. - resetPayloadReaders(); + for (int i = 0; i < tsPayloadReaders.size(); i++) { + tsPayloadReaders.valueAt(i).seek(); + } bytesSinceLastSync = 0; } @@ -217,48 +241,42 @@ public final class TsExtractor implements Extractor { } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) + public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { - byte[] data = tsPacketBuffer.data; - - // Shift bytes to the start of the buffer if there isn't enough space left at the end. - if (BUFFER_SIZE - tsPacketBuffer.getPosition() < TS_PACKET_SIZE) { - int bytesLeft = tsPacketBuffer.bytesLeft(); - if (bytesLeft > 0) { - System.arraycopy(data, tsPacketBuffer.getPosition(), data, 0, bytesLeft); + if (tracksEnded) { + long inputLength = input.getLength(); + boolean canReadDuration = inputLength != C.LENGTH_UNSET && mode != MODE_HLS; + if (canReadDuration && !durationReader.isDurationReadFinished()) { + return durationReader.readDuration(input, seekPosition, pcrPid); + } + maybeOutputSeekMap(inputLength); + + if (pendingSeekToStart) { + pendingSeekToStart = false; + seek(/* position= */ 0, /* timeUs= */ 0); + if (input.getPosition() != 0) { + seekPosition.position = 0; + return RESULT_SEEK; + } + } + + if (tsBinarySearchSeeker != null && tsBinarySearchSeeker.isSeeking()) { + return tsBinarySearchSeeker.handlePendingSeek( + input, seekPosition, /* outputFrameHolder= */ null); } - tsPacketBuffer.reset(data, bytesLeft); } - // Read more bytes until we have at least one packet. - while (tsPacketBuffer.bytesLeft() < TS_PACKET_SIZE) { - int limit = tsPacketBuffer.limit(); - int read = input.read(data, limit, BUFFER_SIZE - limit); - if (read == C.RESULT_END_OF_INPUT) { - return RESULT_END_OF_INPUT; - } - tsPacketBuffer.setLimit(limit + read); + if (!fillBufferWithAtLeastOnePacket(input)) { + return RESULT_END_OF_INPUT; + } + + int endOfPacket = findEndOfFirstTsPacketInBuffer(); + int limit = tsPacketBuffer.limit(); + if (endOfPacket > limit) { + return RESULT_CONTINUE; } // Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format. - int limit = tsPacketBuffer.limit(); - int position = tsPacketBuffer.getPosition(); - int searchStart = position; - while (position < limit && data[position] != TS_SYNC_BYTE) { - position++; - } - tsPacketBuffer.setPosition(position); - - int endOfPacket = position + TS_PACKET_SIZE; - if (endOfPacket > limit) { - bytesSinceLastSync += position - searchStart; - if (mode == MODE_HLS && bytesSinceLastSync > TS_PACKET_SIZE * 2) { - throw new ParserException("Cannot find sync byte. Most likely not a Transport Stream."); - } - return RESULT_CONTINUE; - } - bytesSinceLastSync = 0; - int tsPacketHeader = tsPacketBuffer.readInt(); if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator // There are uncorrectable errors in this packet. @@ -300,9 +318,18 @@ public final class TsExtractor implements Extractor { } // Read the payload. - tsPacketBuffer.setLimit(endOfPacket); - payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator); - tsPacketBuffer.setLimit(limit); + boolean wereTracksEnded = tracksEnded; + if (shouldConsumePacketPayload(pid)) { + tsPacketBuffer.setLimit(endOfPacket); + payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator); + tsPacketBuffer.setLimit(limit); + } + if (mode != MODE_HLS && !wereTracksEnded && tracksEnded) { + // We have read all tracks from all PMTs in this stream. Now seek to the beginning and read + // again to make sure we output all media, including any contained in packets prior to those + // containing the track information. + pendingSeekToStart = true; + } tsPacketBuffer.setPosition(endOfPacket); return RESULT_CONTINUE; @@ -310,6 +337,78 @@ public final class TsExtractor implements Extractor { // Internals. + private void maybeOutputSeekMap(long inputLength) { + if (!hasOutputSeekMap) { + hasOutputSeekMap = true; + if (durationReader.getDurationUs() != C.TIME_UNSET) { + tsBinarySearchSeeker = + new TsBinarySearchSeeker( + durationReader.getPcrTimestampAdjuster(), + durationReader.getDurationUs(), + inputLength, + pcrPid); + output.seekMap(tsBinarySearchSeeker.getSeekMap()); + } else { + output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs())); + } + } + } + + private boolean fillBufferWithAtLeastOnePacket(ExtractorInput input) + throws IOException, InterruptedException { + byte[] data = tsPacketBuffer.data; + // Shift bytes to the start of the buffer if there isn't enough space left at the end. + if (BUFFER_SIZE - tsPacketBuffer.getPosition() < TS_PACKET_SIZE) { + int bytesLeft = tsPacketBuffer.bytesLeft(); + if (bytesLeft > 0) { + System.arraycopy(data, tsPacketBuffer.getPosition(), data, 0, bytesLeft); + } + tsPacketBuffer.reset(data, bytesLeft); + } + // Read more bytes until we have at least one packet. + while (tsPacketBuffer.bytesLeft() < TS_PACKET_SIZE) { + int limit = tsPacketBuffer.limit(); + int read = input.read(data, limit, BUFFER_SIZE - limit); + if (read == C.RESULT_END_OF_INPUT) { + return false; + } + tsPacketBuffer.setLimit(limit + read); + } + return true; + } + + /** + * Returns the position of the end of the first TS packet (exclusive) in the packet buffer. + * + *

    This may be a position beyond the buffer limit if the packet has not been read fully into + * the buffer, or if no packet could be found within the buffer. + */ + private int findEndOfFirstTsPacketInBuffer() throws ParserException { + int searchStart = tsPacketBuffer.getPosition(); + int limit = tsPacketBuffer.limit(); + int syncBytePosition = TsUtil.findSyncBytePosition(tsPacketBuffer.data, searchStart, limit); + // Discard all bytes before the sync byte. + // If sync byte is not found, this means discard the whole buffer. + tsPacketBuffer.setPosition(syncBytePosition); + int endOfPacket = syncBytePosition + TS_PACKET_SIZE; + if (endOfPacket > limit) { + bytesSinceLastSync += syncBytePosition - searchStart; + if (mode == MODE_HLS && bytesSinceLastSync > TS_PACKET_SIZE * 2) { + throw new ParserException("Cannot find sync byte. Most likely not a Transport Stream."); + } + } else { + // We have found a packet within the buffer. + bytesSinceLastSync = 0; + } + return endOfPacket; + } + + private boolean shouldConsumePacketPayload(int packetPid) { + return mode == MODE_HLS + || tracksEnded + || !trackPids.get(packetPid, /* valueIfKeyNotFound= */ false); // It's a PSI packet + } + private void resetPayloadReaders() { trackIds.clear(); tsPayloadReaders.clear(); @@ -422,9 +521,16 @@ public final class TsExtractor implements Extractor { // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12) sectionData.skipBytes(2); int programNumber = sectionData.readUnsignedShort(); + + // Skip 3 bytes (24 bits), including: // reserved (2), version_number (5), current_next_indicator (1), section_number (8), - // last_section_number (8), reserved (3), PCR_PID (13) - sectionData.skipBytes(5); + // last_section_number (8) + sectionData.skipBytes(3); + + sectionData.readBytes(pmtScratch, 2); + // reserved (3), PCR_PID (13) + pmtScratch.skipBits(3); + pcrPid = pmtScratch.readBits(13); // Read program_info_length. sectionData.readBytes(pmtScratch, 2); @@ -476,14 +582,16 @@ public final class TsExtractor implements Extractor { int trackIdCount = trackIdToPidScratch.size(); for (int i = 0; i < trackIdCount; i++) { int trackId = trackIdToPidScratch.keyAt(i); + int trackPid = trackIdToPidScratch.valueAt(i); trackIds.put(trackId, true); + trackPids.put(trackPid, true); TsPayloadReader reader = trackIdToReaderScratch.valueAt(i); if (reader != null) { if (reader != id3Reader) { reader.init(timestampAdjuster, output, new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE)); } - tsPayloadReaders.put(trackIdToPidScratch.valueAt(i), reader); + tsPayloadReaders.put(trackPid, reader); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index efa764b572..2ea25bb2e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -77,8 +77,10 @@ public interface TsPayloadReader { byte[] descriptorBytes) { this.streamType = streamType; this.language = language; - this.dvbSubtitleInfos = dvbSubtitleInfos == null ? Collections.emptyList() - : Collections.unmodifiableList(dvbSubtitleInfos); + this.dvbSubtitleInfos = + dvbSubtitleInfos == null + ? Collections.emptyList() + : Collections.unmodifiableList(dvbSubtitleInfos); this.descriptorBytes = descriptorBytes; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsUtil.java new file mode 100644 index 0000000000..2a7a0d25ab --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsUtil.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.exoplayer2.extractor.ts; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.ParsableByteArray; + +/** Utilities method for extracting MPEG-TS streams. */ +public final class TsUtil { + /** + * Returns the position of the first TS_SYNC_BYTE within the range [startPosition, limitPosition) + * from the provided data array, or returns limitPosition if sync byte could not be found. + */ + public static int findSyncBytePosition(byte[] data, int startPosition, int limitPosition) { + int position = startPosition; + while (position < limitPosition && data[position] != TsExtractor.TS_SYNC_BYTE) { + position++; + } + return position; + } + + /** + * Returns the PCR value read from a given TS packet. + * + * @param packetBuffer The buffer that holds the packet. + * @param startOfPacket The starting position of the packet in the buffer. + * @param pcrPid The PID for valid packets that contain PCR values. + * @return The PCR value read from the packet, if its PID is equal to {@code pcrPid} and it + * contains a valid PCR value. Returns {@link C#TIME_UNSET} otherwise. + */ + public static long readPcrFromPacket( + ParsableByteArray packetBuffer, int startOfPacket, int pcrPid) { + packetBuffer.setPosition(startOfPacket); + if (packetBuffer.bytesLeft() < 5) { + // Header = 4 bytes, adaptationFieldLength = 1 byte. + return C.TIME_UNSET; + } + // Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format. + int tsPacketHeader = packetBuffer.readInt(); + if ((tsPacketHeader & 0x800000) != 0) { + // transport_error_indicator != 0 means there are uncorrectable errors in this packet. + return C.TIME_UNSET; + } + int pid = (tsPacketHeader & 0x1FFF00) >> 8; + if (pid != pcrPid) { + return C.TIME_UNSET; + } + boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0; + if (!adaptationFieldExists) { + return C.TIME_UNSET; + } + + int adaptationFieldLength = packetBuffer.readUnsignedByte(); + if (adaptationFieldLength >= 7 && packetBuffer.bytesLeft() >= 7) { + int flags = packetBuffer.readUnsignedByte(); + boolean pcrFlagSet = (flags & 0x10) == 0x10; + if (pcrFlagSet) { + byte[] pcrBytes = new byte[6]; + packetBuffer.readBytes(pcrBytes, /* offset= */ 0, pcrBytes.length); + return readPcrValueFromPcrBytes(pcrBytes); + } + } + return C.TIME_UNSET; + } + + /** + * Returns the value of PCR base - first 33 bits in big endian order from the PCR bytes. + * + *

    We ignore PCR Ext, because it's too small to have any significance. + */ + private static long readPcrValueFromPcrBytes(byte[] pcrBytes) { + return (pcrBytes[0] & 0xFFL) << 25 + | (pcrBytes[1] & 0xFFL) << 17 + | (pcrBytes[2] & 0xFFL) << 9 + | (pcrBytes[3] & 0xFFL) << 1 + | (pcrBytes[4] & 0xFFL) >> 7; + } + + private TsUtil() { + // Prevent instantiation. + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java new file mode 100644 index 0000000000..724eba1d9a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.ts; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.text.cea.CeaUtil; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.List; + +/** Consumes user data, outputting contained CEA-608/708 messages to a {@link TrackOutput}. */ +/* package */ final class UserDataReader { + + private static final int USER_DATA_START_CODE = 0x0001B2; + + private final List closedCaptionFormats; + private final TrackOutput[] outputs; + + public UserDataReader(List closedCaptionFormats) { + this.closedCaptionFormats = closedCaptionFormats; + outputs = new TrackOutput[closedCaptionFormats.size()]; + } + + public void createTracks( + ExtractorOutput extractorOutput, TsPayloadReader.TrackIdGenerator idGenerator) { + for (int i = 0; i < outputs.length; i++) { + idGenerator.generateNewId(); + TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); + Format channelFormat = closedCaptionFormats.get(i); + String channelMimeType = channelFormat.sampleMimeType; + Assertions.checkArgument( + MimeTypes.APPLICATION_CEA608.equals(channelMimeType) + || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), + "Invalid closed caption mime type provided: " + channelMimeType); + output.format( + Format.createTextSampleFormat( + idGenerator.getFormatId(), + channelMimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + channelFormat.selectionFlags, + channelFormat.language, + channelFormat.accessibilityChannel, + /* drmInitData= */ null, + Format.OFFSET_SAMPLE_RELATIVE, + channelFormat.initializationData)); + outputs[i] = output; + } + } + + public void consume(long pesTimeUs, ParsableByteArray userDataPayload) { + if (userDataPayload.bytesLeft() < 9) { + return; + } + int userDataStartCode = userDataPayload.readInt(); + int userDataIdentifier = userDataPayload.readInt(); + int userDataTypeCode = userDataPayload.readUnsignedByte(); + if (userDataStartCode == USER_DATA_START_CODE + && userDataIdentifier == CeaUtil.USER_DATA_IDENTIFIER_GA94 + && userDataTypeCode == CeaUtil.USER_DATA_TYPE_CODE_MPEG_CC) { + CeaUtil.consumeCcData(pesTimeUs, userDataPayload, outputs); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index 4f2be71a69..7d6aa7024c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -32,17 +32,8 @@ import java.io.IOException; */ public final class WavExtractor implements Extractor { - /** - * Factory for {@link WavExtractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { - - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new WavExtractor()}; - } - - }; + /** Factory for {@link WavExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new WavExtractor()}; /** Arbitrary maximum input size of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */ private static final int MAX_INPUT_SIZE = 32 * 1024; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index d0810a0629..284b750107 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.wav; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.audio.WavUtil; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -25,17 +26,10 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */ -/*package*/ final class WavHeaderReader { +/* package */ final class WavHeaderReader { private static final String TAG = "WavHeaderReader"; - /** Integer PCM audio data. */ - private static final int TYPE_PCM = 0x0001; - /** Float PCM audio data. */ - private static final int TYPE_FLOAT = 0x0003; - /** Extended WAVE format. */ - private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; - /** * Peeks and returns a {@code WavHeader}. * @@ -54,21 +48,21 @@ import java.io.IOException; // Attempt to read the RIFF chunk. ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); - if (chunkHeader.id != Util.getIntegerCodeForString("RIFF")) { + if (chunkHeader.id != WavUtil.RIFF_FOURCC) { return null; } input.peekFully(scratch.data, 0, 4); scratch.setPosition(0); int riffFormat = scratch.readInt(); - if (riffFormat != Util.getIntegerCodeForString("WAVE")) { + if (riffFormat != WavUtil.WAVE_FOURCC) { Log.e(TAG, "Unsupported RIFF format: " + riffFormat); return null; } // Skip chunks until we find the format chunk. chunkHeader = ChunkHeader.peek(input, scratch); - while (chunkHeader.id != Util.getIntegerCodeForString("fmt ")) { + while (chunkHeader.id != WavUtil.FMT_FOURCC) { input.advancePeekPosition((int) chunkHeader.size); chunkHeader = ChunkHeader.peek(input, scratch); } @@ -89,22 +83,9 @@ import java.io.IOException; + blockAlignment); } - @C.PcmEncoding int encoding; - switch (type) { - case TYPE_PCM: - case TYPE_WAVE_FORMAT_EXTENSIBLE: - encoding = Util.getPcmEncoding(bitsPerSample); - break; - case TYPE_FLOAT: - encoding = bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID; - break; - default: - Log.e(TAG, "Unsupported WAV format type: " + type); - return null; - } - + @C.PcmEncoding int encoding = WavUtil.getEncodingForType(type, bitsPerSample); if (encoding == C.ENCODING_INVALID) { - Log.e(TAG, "Unsupported WAV bit depth " + bitsPerSample + " for type " + type); + Log.e(TAG, "Unsupported WAV format: " + bitsPerSample + " bit/sample, type " + type); return null; } @@ -158,6 +139,10 @@ import java.io.IOException; wavHeader.setDataBounds(input.getPosition(), chunkHeader.size); } + private WavHeaderReader() { + // Prevent instantiation. + } + /** Container for a WAV chunk header. */ private static final class ChunkHeader { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index d822916bce..727dfaf1d5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -30,10 +30,9 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; -/** - * Information about a {@link MediaCodec} for a given mime type. - */ +/** Information about a {@link MediaCodec} for a given mime type. */ @TargetApi(16) +@SuppressWarnings("InlinedApi") public final class MediaCodecInfo { public static final String TAG = "MediaCodecInfo"; @@ -88,6 +87,8 @@ public final class MediaCodecInfo { /** Whether this instance describes a passthrough codec. */ public final boolean passthrough; + private final boolean isVideo; + /** * Creates an instance representing an audio passthrough decoder. * @@ -157,6 +158,12 @@ public final class MediaCodecInfo { adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities); tunneling = capabilities != null && isTunneling(capabilities); secure = forceSecure || (capabilities != null && isSecure(capabilities)); + isVideo = MimeTypes.isVideo(mimeType); + } + + @Override + public String toString() { + return name; } /** @@ -182,6 +189,41 @@ public final class MediaCodecInfo { : getMaxSupportedInstancesV23(capabilities); } + /** + * Returns whether the decoder may support decoding the given {@code format}. + * + * @param format The input media format. + * @return Whether the decoder may support decoding the given {@code format}. + * @throws MediaCodecUtil.DecoderQueryException Thrown if an error occurs while querying decoders. + */ + public boolean isFormatSupported(Format format) throws MediaCodecUtil.DecoderQueryException { + if (!isCodecSupported(format.codecs)) { + return false; + } + + if (isVideo) { + if (format.width <= 0 || format.height <= 0) { + return true; + } + if (Util.SDK_INT >= 21) { + return isVideoSizeAndRateSupportedV21(format.width, format.height, format.frameRate); + } else { + boolean isFormatSupported = + format.width * format.height <= MediaCodecUtil.maxH264DecodableFrameSize(); + if (!isFormatSupported) { + logNoSupport("legacyFrameSize, " + format.width + "x" + format.height); + } + return isFormatSupported; + } + } else { // Audio + return Util.SDK_INT < 21 + || ((format.sampleRate == Format.NO_VALUE + || isAudioSampleRateSupportedV21(format.sampleRate)) + && (format.channelCount == Format.NO_VALUE + || isAudioChannelCountSupportedV21(format.channelCount))); + } + } + /** * Whether the decoder supports the given {@code codec}. If there is insufficient information to * decide, returns true. @@ -216,6 +258,63 @@ public final class MediaCodecInfo { return false; } + /** + * Returns whether it may be possible to adapt to playing a different format when the codec is + * configured to play media in the specified {@code format}. For adaptation to succeed, the codec + * must also be configured with appropriate maximum values and {@link + * #isSeamlessAdaptationSupported(Format, Format)} must return {@code true} for the old/new + * formats. + * + * @param format The format of media for which the decoder will be configured. + * @return Whether adaptation may be possible + */ + public boolean isSeamlessAdaptationSupported(Format format) { + if (isVideo) { + return adaptive; + } else { + Pair codecProfileLevel = + MediaCodecUtil.getCodecProfileAndLevel(format.codecs); + return codecProfileLevel != null && codecProfileLevel.first == CodecProfileLevel.AACObjectXHE; + } + } + + /** + * Returns whether it is possible to adapt the decoder seamlessly from {@code oldFormat} to {@code + * newFormat}. + * + * @param oldFormat The format being decoded. + * @param newFormat The new format. + * @return Whether it is possible to adapt the decoder seamlessly. + */ + public boolean isSeamlessAdaptationSupported(Format oldFormat, Format newFormat) { + if (isVideo) { + return oldFormat.sampleMimeType.equals(newFormat.sampleMimeType) + && oldFormat.rotationDegrees == newFormat.rotationDegrees + && (adaptive + || (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height)) + && Util.areEqual(oldFormat.colorInfo, newFormat.colorInfo); + } else { + if (!MimeTypes.AUDIO_AAC.equals(mimeType) + || !oldFormat.sampleMimeType.equals(newFormat.sampleMimeType) + || oldFormat.channelCount != newFormat.channelCount + || oldFormat.sampleRate != newFormat.sampleRate) { + return false; + } + // Check the codec profile levels support adaptation. + Pair oldCodecProfileLevel = + MediaCodecUtil.getCodecProfileAndLevel(oldFormat.codecs); + Pair newCodecProfileLevel = + MediaCodecUtil.getCodecProfileAndLevel(newFormat.codecs); + if (oldCodecProfileLevel == null || newCodecProfileLevel == null) { + return false; + } + int oldProfile = oldCodecProfileLevel.first; + int newProfile = newCodecProfileLevel.first; + return oldProfile == CodecProfileLevel.AACObjectXHE + && newProfile == CodecProfileLevel.AACObjectXHE; + } + } + /** * Whether the decoder supports video with a given width, height and frame rate. *

    diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 03a0b66661..3630977fca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -21,8 +21,10 @@ import android.media.MediaCodec.CodecException; import android.media.MediaCodec.CryptoException; import android.media.MediaCrypto; import android.media.MediaFormat; +import android.os.Bundle; import android.os.Looper; import android.os.SystemClock; +import android.support.annotation.CheckResult; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.util.Log; @@ -46,6 +48,7 @@ import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; @@ -84,22 +87,64 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ public final String diagnosticInfo; + /** + * If the decoder failed to initialize and another decoder being used as a fallback also failed + * to initialize, the {@link DecoderInitializationException} for the fallback decoder. Null if + * there was no fallback decoder or no suitable decoders were found. + */ + public final @Nullable DecoderInitializationException fallbackDecoderInitializationException; + public DecoderInitializationException(Format format, Throwable cause, boolean secureDecoderRequired, int errorCode) { - super("Decoder init failed: [" + errorCode + "], " + format, cause); - this.mimeType = format.sampleMimeType; - this.secureDecoderRequired = secureDecoderRequired; - this.decoderName = null; - this.diagnosticInfo = buildCustomDiagnosticInfo(errorCode); + this( + "Decoder init failed: [" + errorCode + "], " + format, + cause, + format.sampleMimeType, + secureDecoderRequired, + /* decoderName= */ null, + buildCustomDiagnosticInfo(errorCode), + /* fallbackDecoderInitializationException= */ null); } public DecoderInitializationException(Format format, Throwable cause, boolean secureDecoderRequired, String decoderName) { - super("Decoder init failed: " + decoderName + ", " + format, cause); - this.mimeType = format.sampleMimeType; + this( + "Decoder init failed: " + decoderName + ", " + format, + cause, + format.sampleMimeType, + secureDecoderRequired, + decoderName, + Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null, + /* fallbackDecoderInitializationException= */ null); + } + + private DecoderInitializationException( + String message, + Throwable cause, + String mimeType, + boolean secureDecoderRequired, + @Nullable String decoderName, + @Nullable String diagnosticInfo, + @Nullable DecoderInitializationException fallbackDecoderInitializationException) { + super(message, cause); + this.mimeType = mimeType; this.secureDecoderRequired = secureDecoderRequired; this.decoderName = decoderName; - this.diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null; + this.diagnosticInfo = diagnosticInfo; + this.fallbackDecoderInitializationException = fallbackDecoderInitializationException; + } + + @CheckResult + private DecoderInitializationException copyWithFallbackException( + DecoderInitializationException fallbackException) { + return new DecoderInitializationException( + getMessage(), + getCause(), + mimeType, + secureDecoderRequired, + decoderName, + diagnosticInfo, + fallbackException); } @TargetApi(21) @@ -117,6 +162,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } + /** Indicates no codec operating rate should be set. */ + protected static final float CODEC_OPERATING_RATE_UNSET = -1; + private static final String TAG = "MediaCodecRenderer"; /** @@ -220,6 +268,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Nullable private final DrmSessionManager drmSessionManager; private final boolean playClearSamplesWithoutKeys; + private final float assumedMinimumCodecOperatingRate; private final DecoderInputBuffer buffer; private final DecoderInputBuffer flagsOnlyBuffer; private final FormatHolder formatHolder; @@ -230,7 +279,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private DrmSession drmSession; private DrmSession pendingDrmSession; private MediaCodec codec; - private MediaCodecInfo codecInfo; + private float rendererOperatingRate; + private float codecOperatingRate; + private boolean codecConfiguredWithOperatingRate; + private @Nullable ArrayDeque availableCodecInfos; + private @Nullable DecoderInitializationException preferredDecoderInitializationException; + private @Nullable MediaCodecInfo codecInfo; private @AdaptationWorkaroundMode int codecAdaptationWorkaroundMode; private boolean codecNeedsDiscardToSpsWorkaround; private boolean codecNeedsFlushWorkaround; @@ -271,15 +325,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * begin in parallel with key acquisition. This parameter specifies whether the renderer is * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param assumedMinimumCodecOperatingRate A codec operating rate that all codecs instantiated by + * this renderer are assumed to meet implicitly (i.e. without the operating rate being set + * explicitly using {@link MediaFormat#KEY_OPERATING_RATE}). */ - public MediaCodecRenderer(int trackType, MediaCodecSelector mediaCodecSelector, + public MediaCodecRenderer( + int trackType, + MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys) { + boolean playClearSamplesWithoutKeys, + float assumedMinimumCodecOperatingRate) { super(trackType); Assertions.checkState(Util.SDK_INT >= 16); this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector); this.drmSessionManager = drmSessionManager; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + this.assumedMinimumCodecOperatingRate = assumedMinimumCodecOperatingRate; buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); formatHolder = new FormatHolder(); @@ -287,6 +348,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputBufferInfo = new MediaCodec.BufferInfo(); codecReconfigurationState = RECONFIGURATION_STATE_NONE; codecReinitializationState = REINITIALIZATION_STATE_NONE; + codecOperatingRate = CODEC_OPERATING_RATE_UNSET; + rendererOperatingRate = 1f; } @Override @@ -318,18 +381,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer { throws DecoderQueryException; /** - * Returns a {@link MediaCodecInfo} for a given format. + * Returns a list of decoders that can decode media in the specified format, in priority order. * * @param mediaCodecSelector The decoder selector. * @param format The format for which a decoder is required. * @param requiresSecureDecoder Whether a secure decoder is required. - * @return A {@link MediaCodecInfo} describing the decoder to instantiate, or null if no - * suitable decoder exists. + * @return A list of {@link MediaCodecInfo}s corresponding to decoders. May be empty. * @throws DecoderQueryException Thrown if there was an error querying decoders. */ - protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector, - Format format, boolean requiresSecureDecoder) throws DecoderQueryException { - return mediaCodecSelector.getDecoderInfo(format.sampleMimeType, requiresSecureDecoder); + protected List getDecoderInfos( + MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder) + throws DecoderQueryException { + return mediaCodecSelector.getDecoderInfos(format.sampleMimeType, requiresSecureDecoder); } /** @@ -339,10 +402,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * @param codec The {@link MediaCodec} to configure. * @param format The format for which the codec is being configured. * @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption. + * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if + * no codec operating rate should be set. * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ - protected abstract void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, - MediaCrypto crypto) throws DecoderQueryException; + protected abstract void configureCodec( + MediaCodecInfo codecInfo, + MediaCodec codec, + Format format, + MediaCrypto crypto, + float codecOperatingRate) + throws DecoderQueryException; protected final void maybeInitCodec() throws ExoPlaybackException { if (codec != null || format == null) { @@ -369,78 +439,44 @@ public abstract class MediaCodecRenderer extends BaseRenderer { wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto(); drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType); } - } - - if (codecInfo == null) { - try { - codecInfo = getDecoderInfo(mediaCodecSelector, format, drmSessionRequiresSecureDecoder); - if (codecInfo == null && drmSessionRequiresSecureDecoder) { - // The drm session indicates that a secure decoder is required, but the device does not - // have one. Assuming that supportsFormat indicated support for the media being played, we - // know that it does not require a secure output path. Most CDM implementations allow - // playback to proceed with a non-secure decoder in this case, so we try our luck. - codecInfo = getDecoderInfo(mediaCodecSelector, format, false); - if (codecInfo != null) { - Log.w(TAG, "Drm session requires secure decoder for " + mimeType + ", but " - + "no secure decoder available. Trying to proceed with " + codecInfo.name + "."); - } + if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) { + @DrmSession.State int drmSessionState = drmSession.getState(); + if (drmSessionState == DrmSession.STATE_ERROR) { + throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + } else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) { + // Wait for keys. + return; } - } catch (DecoderQueryException e) { - throwDecoderInitError(new DecoderInitializationException(format, e, - drmSessionRequiresSecureDecoder, DecoderInitializationException.DECODER_QUERY_ERROR)); - } - - if (codecInfo == null) { - throwDecoderInitError(new DecoderInitializationException(format, null, - drmSessionRequiresSecureDecoder, - DecoderInitializationException.NO_SUITABLE_DECODER_ERROR)); } } - if (!shouldInitCodec(codecInfo)) { - return; + try { + if (!initCodecWithFallback(wrappedMediaCrypto, drmSessionRequiresSecureDecoder)) { + // We can't initialize a codec yet. + return; + } + } catch (DecoderInitializationException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); } String codecName = codecInfo.name; codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName); codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); - codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName); + codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecInfo); codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format); - try { - long codecInitializingTimestamp = SystemClock.elapsedRealtime(); - TraceUtil.beginSection("createCodec:" + codecName); - codec = MediaCodec.createByCodecName(codecName); - TraceUtil.endSection(); - TraceUtil.beginSection("configureCodec"); - configureCodec(codecInfo, codec, format, wrappedMediaCrypto); - TraceUtil.endSection(); - TraceUtil.beginSection("startCodec"); - codec.start(); - TraceUtil.endSection(); - long codecInitializedTimestamp = SystemClock.elapsedRealtime(); - onCodecInitialized(codecName, codecInitializedTimestamp, - codecInitializedTimestamp - codecInitializingTimestamp); - getCodecBuffers(); - } catch (Exception e) { - throwDecoderInitError(new DecoderInitializationException(format, e, - drmSessionRequiresSecureDecoder, codecName)); - } - codecHotswapDeadlineMs = getState() == STATE_STARTED - ? (SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS) : C.TIME_UNSET; + codecHotswapDeadlineMs = + getState() == STATE_STARTED + ? (SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS) + : C.TIME_UNSET; resetInputBuffer(); resetOutputBuffer(); waitingForFirstSyncFrame = true; decoderCounters.decoderInitCount++; } - private void throwDecoderInitError(DecoderInitializationException e) - throws ExoPlaybackException { - throw ExoPlaybackException.createForRenderer(e, getIndex()); - } - protected boolean shouldInitCodec(MediaCodecInfo codecInfo) { return true; } @@ -467,9 +503,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } + @Override + public final void setOperatingRate(float operatingRate) throws ExoPlaybackException { + rendererOperatingRate = operatingRate; + updateCodecOperatingRate(); + } + @Override protected void onDisabled() { format = null; + availableCodecInfos = null; try { releaseCodec(); } finally { @@ -512,6 +555,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecReceivedEos = false; codecReconfigurationState = RECONFIGURATION_STATE_NONE; codecReinitializationState = REINITIALIZATION_STATE_NONE; + codecConfiguredWithOperatingRate = false; if (codec != null) { decoderCounters.decoderReleaseCount++; try { @@ -622,6 +666,166 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } + private boolean initCodecWithFallback(MediaCrypto crypto, boolean drmSessionRequiresSecureDecoder) + throws DecoderInitializationException { + if (availableCodecInfos == null) { + try { + availableCodecInfos = + new ArrayDeque<>(getAvailableCodecInfos(drmSessionRequiresSecureDecoder)); + preferredDecoderInitializationException = null; + } catch (DecoderQueryException e) { + throw new DecoderInitializationException( + format, + e, + drmSessionRequiresSecureDecoder, + DecoderInitializationException.DECODER_QUERY_ERROR); + } + } + + if (availableCodecInfos.isEmpty()) { + throw new DecoderInitializationException( + format, + /* cause= */ null, + drmSessionRequiresSecureDecoder, + DecoderInitializationException.NO_SUITABLE_DECODER_ERROR); + } + + while (true) { + MediaCodecInfo codecInfo = availableCodecInfos.peekFirst(); + if (!shouldInitCodec(codecInfo)) { + return false; + } + try { + initCodec(codecInfo, crypto); + return true; + } catch (Exception e) { + Log.w(TAG, "Failed to initialize decoder: " + codecInfo, e); + // This codec failed to initialize, so fall back to the next codec in the list (if any). We + // won't try to use this codec again unless there's a format change or the renderer is + // disabled and re-enabled. + availableCodecInfos.removeFirst(); + DecoderInitializationException exception = + new DecoderInitializationException( + format, e, drmSessionRequiresSecureDecoder, codecInfo.name); + if (preferredDecoderInitializationException == null) { + preferredDecoderInitializationException = exception; + } else { + preferredDecoderInitializationException = + preferredDecoderInitializationException.copyWithFallbackException(exception); + } + if (availableCodecInfos.isEmpty()) { + throw preferredDecoderInitializationException; + } + } + } + } + + private List getAvailableCodecInfos(boolean drmSessionRequiresSecureDecoder) + throws DecoderQueryException { + List codecInfos = + getDecoderInfos(mediaCodecSelector, format, drmSessionRequiresSecureDecoder); + if (codecInfos.isEmpty() && drmSessionRequiresSecureDecoder) { + // The drm session indicates that a secure decoder is required, but the device does not + // have one. Assuming that supportsFormat indicated support for the media being played, we + // know that it does not require a secure output path. Most CDM implementations allow + // playback to proceed with a non-secure decoder in this case, so we try our luck. + codecInfos = getDecoderInfos(mediaCodecSelector, format, /* requiresSecureDecoder= */ false); + if (!codecInfos.isEmpty()) { + Log.w( + TAG, + "Drm session requires secure decoder for " + + format.sampleMimeType + + ", but no secure decoder available. Trying to proceed with " + + codecInfos + + "."); + } + } + return codecInfos; + } + + private void initCodec(MediaCodecInfo codecInfo, MediaCrypto crypto) throws Exception { + long codecInitializingTimestamp; + long codecInitializedTimestamp; + MediaCodec codec = null; + String name = codecInfo.name; + updateCodecOperatingRate(); + boolean configureWithOperatingRate = codecOperatingRate > assumedMinimumCodecOperatingRate; + try { + codecInitializingTimestamp = SystemClock.elapsedRealtime(); + TraceUtil.beginSection("createCodec:" + name); + codec = MediaCodec.createByCodecName(name); + TraceUtil.endSection(); + TraceUtil.beginSection("configureCodec"); + configureCodec( + codecInfo, + codec, + format, + crypto, + configureWithOperatingRate ? codecOperatingRate : CODEC_OPERATING_RATE_UNSET); + codecConfiguredWithOperatingRate = configureWithOperatingRate; + TraceUtil.endSection(); + TraceUtil.beginSection("startCodec"); + codec.start(); + TraceUtil.endSection(); + codecInitializedTimestamp = SystemClock.elapsedRealtime(); + getCodecBuffers(codec); + } catch (Exception e) { + if (codec != null) { + resetCodecBuffers(); + codec.release(); + } + throw e; + } + this.codec = codec; + this.codecInfo = codecInfo; + long elapsed = codecInitializedTimestamp - codecInitializingTimestamp; + onCodecInitialized(name, codecInitializedTimestamp, elapsed); + } + + private void getCodecBuffers(MediaCodec codec) { + if (Util.SDK_INT < 21) { + inputBuffers = codec.getInputBuffers(); + outputBuffers = codec.getOutputBuffers(); + } + } + + private void resetCodecBuffers() { + if (Util.SDK_INT < 21) { + inputBuffers = null; + outputBuffers = null; + } + } + + private ByteBuffer getInputBuffer(int inputIndex) { + if (Util.SDK_INT >= 21) { + return codec.getInputBuffer(inputIndex); + } else { + return inputBuffers[inputIndex]; + } + } + + private ByteBuffer getOutputBuffer(int outputIndex) { + if (Util.SDK_INT >= 21) { + return codec.getOutputBuffer(outputIndex); + } else { + return outputBuffers[outputIndex]; + } + } + + private boolean hasOutputBuffer() { + return outputIndex >= 0; + } + + private void resetInputBuffer() { + inputIndex = C.INDEX_UNSET; + buffer.data = null; + } + + private void resetOutputBuffer() { + outputIndex = C.INDEX_UNSET; + outputBuffer = null; + } + /** * @return Whether it may be possible to feed more input data. * @throws ExoPlaybackException If an error occurs feeding the input buffer. @@ -773,66 +977,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return true; } - private void getCodecBuffers() { - if (Util.SDK_INT < 21) { - inputBuffers = codec.getInputBuffers(); - outputBuffers = codec.getOutputBuffers(); - } - } - - private void resetCodecBuffers() { - if (Util.SDK_INT < 21) { - inputBuffers = null; - outputBuffers = null; - } - } - - private ByteBuffer getInputBuffer(int inputIndex) { - if (Util.SDK_INT >= 21) { - return codec.getInputBuffer(inputIndex); - } else { - return inputBuffers[inputIndex]; - } - } - - private ByteBuffer getOutputBuffer(int outputIndex) { - if (Util.SDK_INT >= 21) { - return codec.getOutputBuffer(outputIndex); - } else { - return outputBuffers[outputIndex]; - } - } - - private boolean hasOutputBuffer() { - return outputIndex >= 0; - } - - private void resetInputBuffer() { - inputIndex = C.INDEX_UNSET; - buffer.data = null; - } - - private void resetOutputBuffer() { - outputIndex = C.INDEX_UNSET; - outputBuffer = null; - } - - private static MediaCodec.CryptoInfo getFrameworkCryptoInfo(DecoderInputBuffer buffer, - int adaptiveReconfigurationBytes) { - MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfoV16(); - if (adaptiveReconfigurationBytes == 0) { - return cryptoInfo; - } - // There must be at least one sub-sample, although numBytesOfClearData is permitted to be - // null if it contains no clear data. Instantiate it if needed, and add the reconfiguration - // bytes to the clear byte count of the first sub-sample. - if (cryptoInfo.numBytesOfClearData == null) { - cryptoInfo.numBytesOfClearData = new int[1]; - } - cryptoInfo.numBytesOfClearData[0] += adaptiveReconfigurationBytes; - return cryptoInfo; - } - private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { return false; @@ -911,14 +1055,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } if (!keepingCodec) { - if (codecReceivedBuffers) { - // Signal end of stream and wait for any final output buffers before re-initialization. - codecReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; - } else { - // There aren't any final output buffers, so perform re-initialization immediately. - releaseCodec(); - maybeInitCodec(); - } + reinitializeCodec(); + } else { + updateCodecOperatingRate(); } } @@ -999,6 +1138,77 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return 0; } + /** + * Returns the {@link MediaFormat#KEY_OPERATING_RATE} value for a given renderer operating rate, + * current format and set of possible stream formats. + * + *

    The default implementation returns {@link #CODEC_OPERATING_RATE_UNSET}. + * + * @param operatingRate The renderer operating rate. + * @param format The format for which the codec is being configured. + * @param streamFormats The possible stream formats. + * @return The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if no codec operating + * rate should be set. + */ + protected float getCodecOperatingRate( + float operatingRate, Format format, Format[] streamFormats) { + return CODEC_OPERATING_RATE_UNSET; + } + + /** + * Updates the codec operating rate, and the codec itself if necessary. + * + * @throws ExoPlaybackException If an error occurs releasing or initializing a codec. + */ + private void updateCodecOperatingRate() throws ExoPlaybackException { + if (format == null || Util.SDK_INT < 23) { + return; + } + + float codecOperatingRate = + getCodecOperatingRate(rendererOperatingRate, format, getStreamFormats()); + if (this.codecOperatingRate == codecOperatingRate) { + return; + } + + this.codecOperatingRate = codecOperatingRate; + if (codec == null || codecReinitializationState != REINITIALIZATION_STATE_NONE) { + // Either no codec, or it's about to be reinitialized anyway. + } else if (codecOperatingRate == CODEC_OPERATING_RATE_UNSET + && codecConfiguredWithOperatingRate) { + // We need to clear the operating rate. The only way to do so is to instantiate a new codec + // instance. See [Internal ref: b/71987865]. + reinitializeCodec(); + } else if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET + && (codecConfiguredWithOperatingRate + || codecOperatingRate > assumedMinimumCodecOperatingRate)) { + // We need to set the operating rate, either because we've set it previously or because it's + // above the assumed minimum rate. + Bundle codecParameters = new Bundle(); + codecParameters.putFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate); + codec.setParameters(codecParameters); + codecConfiguredWithOperatingRate = true; + } + } + + /** + * Starts the process of releasing the existing codec and initializing a new one. This may occur + * immediately, or be deferred until any final output buffers have been dequeued. + * + * @throws ExoPlaybackException If an error occurs releasing or initializing a codec. + */ + private void reinitializeCodec() throws ExoPlaybackException { + availableCodecInfos = null; + if (codecReceivedBuffers) { + // Signal end of stream and wait for any final output buffers before re-initialization. + codecReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; + } else { + // There aren't any final output buffers, so perform re-initialization immediately. + releaseCodec(); + maybeInitCodec(); + } + } + /** * @return Whether it may be possible to drain more output data. * @throws ExoPlaybackException If an error occurs draining the output buffer. @@ -1209,6 +1419,32 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } + private static MediaCodec.CryptoInfo getFrameworkCryptoInfo( + DecoderInputBuffer buffer, int adaptiveReconfigurationBytes) { + MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfoV16(); + if (adaptiveReconfigurationBytes == 0) { + return cryptoInfo; + } + // There must be at least one sub-sample, although numBytesOfClearData is permitted to be + // null if it contains no clear data. Instantiate it if needed, and add the reconfiguration + // bytes to the clear byte count of the first sub-sample. + if (cryptoInfo.numBytesOfClearData == null) { + cryptoInfo.numBytesOfClearData = new int[1]; + } + cryptoInfo.numBytesOfClearData[0] += adaptiveReconfigurationBytes; + return cryptoInfo; + } + + /** + * Returns whether the device needs keys to have been loaded into the {@link DrmSession} before + * codec configuration. + */ + private boolean deviceNeedsDrmKeysToConfigureCodecWorkaround() { + return "Amazon".equals(Util.MANUFACTURER) + && ("AFTM".equals(Util.MODEL) // Fire TV Stick Gen 1 + || "AFTB".equals(Util.MODEL)); // Fire TV Gen 1 + } + /** * Returns whether the decoder is known to fail when flushed. *

    @@ -1272,20 +1508,23 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Returns whether the decoder is known to handle the propagation of the - * {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device. - *

    - * If true is returned, the renderer will work around the issue by approximating end of stream + * Returns whether the decoder is known to handle the propagation of the {@link + * MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device. + * + *

    If true is returned, the renderer will work around the issue by approximating end of stream * behavior without relying on the flag being propagated through to an output buffer by the * underlying decoder. * - * @param name The name of the decoder. + * @param codecInfo Information about the {@link MediaCodec}. * @return True if the decoder is known to handle {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} * propagation incorrectly on the host device. False otherwise. */ - private static boolean codecNeedsEosPropagationWorkaround(String name) { - return Util.SDK_INT <= 17 && ("OMX.rk.video_decoder.avc".equals(name) - || "OMX.allwinner.video.decoder.avc".equals(name)); + private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) { + String name = codecInfo.name; + return (Util.SDK_INT <= 17 + && ("OMX.rk.video_decoder.avc".equals(name) + || "OMX.allwinner.video.decoder.avc".equals(name))) + || ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java index 1823c3a7ff..d92e93d45b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java @@ -16,7 +16,10 @@ package com.google.android.exoplayer2.mediacodec; import android.media.MediaCodec; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import java.util.Collections; +import java.util.List; /** * Selector of {@link MediaCodec} instances. @@ -24,32 +27,58 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryExcep public interface MediaCodecSelector { /** - * Default implementation of {@link MediaCodecSelector}. + * Default implementation of {@link MediaCodecSelector}, which returns the preferred decoder for + * the given format. */ - MediaCodecSelector DEFAULT = new MediaCodecSelector() { + MediaCodecSelector DEFAULT = + new MediaCodecSelector() { + @Override + public List getDecoderInfos(String mimeType, boolean requiresSecureDecoder) + throws DecoderQueryException { + List decoderInfos = + MediaCodecUtil.getDecoderInfos(mimeType, requiresSecureDecoder); + return decoderInfos.isEmpty() + ? Collections.emptyList() + : Collections.singletonList(decoderInfos.get(0)); + } - @Override - public MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder) - throws DecoderQueryException { - return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder); - } - - @Override - public MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { - return MediaCodecUtil.getPassthroughDecoderInfo(); - } - - }; + @Override + public @Nullable MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { + return MediaCodecUtil.getPassthroughDecoderInfo(); + } + }; /** - * Selects a decoder to instantiate for a given mime type. + * A {@link MediaCodecSelector} that returns a list of decoders in priority order, allowing + * fallback to less preferred decoders if initialization fails. * - * @param mimeType The mime type for which a decoder is required. + *

    Note: if a hardware-accelerated video decoder fails to initialize, this selector may provide + * a software video decoder to use as a fallback. Using software decoding can be inefficient, and + * the decoder may be too slow to keep up with the playback position. + */ + MediaCodecSelector DEFAULT_WITH_FALLBACK = + new MediaCodecSelector() { + @Override + public List getDecoderInfos(String mimeType, boolean requiresSecureDecoder) + throws DecoderQueryException { + return MediaCodecUtil.getDecoderInfos(mimeType, requiresSecureDecoder); + } + + @Override + public @Nullable MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { + return MediaCodecUtil.getPassthroughDecoderInfo(); + } + }; + + /** + * Returns a list of decoders that can decode media in the specified MIME type, in priority order. + * + * @param mimeType The MIME type for which a decoder is required. * @param requiresSecureDecoder Whether a secure decoder is required. - * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists. + * @return A list of {@link MediaCodecInfo}s corresponding to decoders. May be empty. * @throws DecoderQueryException Thrown if there was an error querying decoders. */ - MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder) + List getDecoderInfos(String mimeType, boolean requiresSecureDecoder) throws DecoderQueryException; /** @@ -58,6 +87,6 @@ public interface MediaCodecSelector { * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists. * @throws DecoderQueryException Thrown if there was an error querying decoders. */ + @Nullable MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException; - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 347afe29fd..570d5074b7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -57,11 +58,9 @@ public final class MediaCodecUtil { } private static final String TAG = "MediaCodecUtil"; - private static final String GOOGLE_RAW_DECODER_NAME = "OMX.google.raw.decoder"; - private static final String MTK_RAW_DECODER_NAME = "OMX.MTK.AUDIO.DECODER.RAW"; - private static final MediaCodecInfo PASSTHROUGH_DECODER_INFO = - MediaCodecInfo.newPassthroughInstance(GOOGLE_RAW_DECODER_NAME); private static final Pattern PROFILE_PATTERN = Pattern.compile("^\\D?(\\d+)$"); + private static final RawAudioCodecComparator RAW_AUDIO_CODEC_COMPARATOR = + new RawAudioCodecComparator(); private static final HashMap> decoderInfosCache = new HashMap<>(); @@ -75,6 +74,9 @@ public final class MediaCodecUtil { private static final Map HEVC_CODEC_STRING_TO_PROFILE_LEVEL; private static final String CODEC_ID_HEV1 = "hev1"; private static final String CODEC_ID_HVC1 = "hvc1"; + // MP4A AAC. + private static final SparseIntArray MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE; + private static final String CODEC_ID_MP4A = "mp4a"; // Lazily initialized. private static int maxH264DecodableFrameSize = -1; @@ -103,22 +105,21 @@ public final class MediaCodecUtil { /** * Returns information about a decoder suitable for audio passthrough. * - * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder - * exists. + * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists. + * @throws DecoderQueryException If there was an error querying the available decoders. */ - public static MediaCodecInfo getPassthroughDecoderInfo() { - // TODO: Return null if the raw decoder doesn't exist. - return PASSTHROUGH_DECODER_INFO; + public static @Nullable MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { + MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.AUDIO_RAW, /* secure= */ false); + return decoderInfo == null ? null : MediaCodecInfo.newPassthroughInstance(decoderInfo.name); } /** * Returns information about the preferred decoder for a given mime type. * - * @param mimeType The mime type. + * @param mimeType The MIME type. * @param secure Whether the decoder is required to support secure decryption. Always pass false * unless secure decryption really is required. - * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder - * exists. + * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists. * @throws DecoderQueryException If there was an error querying the available decoders. */ public static @Nullable MediaCodecInfo getDecoderInfo(String mimeType, boolean secure) @@ -128,18 +129,18 @@ public final class MediaCodecUtil { } /** - * Returns all {@link MediaCodecInfo}s for the given mime type, in the order given by - * {@link MediaCodecList}. + * Returns all {@link MediaCodecInfo}s for the given mime type, in the order given by {@link + * MediaCodecList}. * - * @param mimeType The mime type. + * @param mimeType The MIME type. * @param secure Whether the decoder is required to support secure decryption. Always pass false * unless secure decryption really is required. - * @return A list of all @{link MediaCodecInfo}s for the given mime type, in the order - * given by {@link MediaCodecList}. + * @return A list of all {@link MediaCodecInfo}s for the given mime type, in the order given by + * {@link MediaCodecList}. * @throws DecoderQueryException If there was an error querying the available decoders. */ - public static synchronized List getDecoderInfos(String mimeType, - boolean secure) throws DecoderQueryException { + public static synchronized List getDecoderInfos(String mimeType, boolean secure) + throws DecoderQueryException { CodecKey key = new CodecKey(mimeType, secure); List cachedDecoderInfos = decoderInfosCache.get(key); if (cachedDecoderInfos != null) { @@ -165,7 +166,7 @@ public final class MediaCodecUtil { getDecoderInfosInternal(eac3Key, mediaCodecList, mimeType); decoderInfos.addAll(eac3DecoderInfos); } - applyWorkarounds(decoderInfos); + applyWorkarounds(mimeType, decoderInfos); List unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos); decoderInfosCache.put(key, unmodifiableDecoderInfos); return unmodifiableDecoderInfos; @@ -201,7 +202,7 @@ public final class MediaCodecUtil { * @return A pair (profile constant, level constant) if {@code codec} is well-formed and * recognized, or null otherwise */ - public static Pair getCodecProfileAndLevel(String codec) { + public static @Nullable Pair getCodecProfileAndLevel(String codec) { if (codec == null) { return null; } @@ -213,6 +214,8 @@ public final class MediaCodecUtil { case CODEC_ID_AVC1: case CODEC_ID_AVC2: return getAvcProfileAndLevel(codec, parts); + case CODEC_ID_MP4A: + return getAacCodecProfileAndLevel(codec, parts); default: return null; } @@ -395,20 +398,12 @@ public final class MediaCodecUtil { * Modifies a list of {@link MediaCodecInfo}s to apply workarounds where we know better than the * platform. * + * @param mimeType The MIME type of input media. * @param decoderInfos The list to modify. */ - private static void applyWorkarounds(List decoderInfos) { - if (Util.SDK_INT < 26 && decoderInfos.size() > 1 - && MTK_RAW_DECODER_NAME.equals(decoderInfos.get(0).name)) { - // Prefer the Google raw decoder over the MediaTek one [Internal: b/62337687]. - for (int i = 1; i < decoderInfos.size(); i++) { - MediaCodecInfo decoderInfo = decoderInfos.get(i); - if (GOOGLE_RAW_DECODER_NAME.equals(decoderInfo.name)) { - decoderInfos.remove(i); - decoderInfos.add(0, decoderInfo); - break; - } - } + private static void applyWorkarounds(String mimeType, List decoderInfos) { + if (MimeTypes.AUDIO_RAW.equals(mimeType)) { + Collections.sort(decoderInfos, RAW_AUDIO_CODEC_COMPARATOR); } } @@ -455,8 +450,8 @@ public final class MediaCodecUtil { return new Pair<>(profile, level); } - private static Pair getAvcProfileAndLevel(String codec, String[] codecsParts) { - if (codecsParts.length < 2) { + private static Pair getAvcProfileAndLevel(String codec, String[] parts) { + if (parts.length < 2) { // The codec has fewer parts than required by the AVC codec string format. Log.w(TAG, "Ignoring malformed AVC codec string: " + codec); return null; @@ -464,14 +459,14 @@ public final class MediaCodecUtil { Integer profileInteger; Integer levelInteger; try { - if (codecsParts[1].length() == 6) { + if (parts[1].length() == 6) { // Format: avc1.xxccyy, where xx is profile and yy level, both hexadecimal. - profileInteger = Integer.parseInt(codecsParts[1].substring(0, 2), 16); - levelInteger = Integer.parseInt(codecsParts[1].substring(4), 16); - } else if (codecsParts.length >= 3) { + profileInteger = Integer.parseInt(parts[1].substring(0, 2), 16); + levelInteger = Integer.parseInt(parts[1].substring(4), 16); + } else if (parts.length >= 3) { // Format: avc1.xx.[y]yy where xx is profile and [y]yy level, both decimal. - profileInteger = Integer.parseInt(codecsParts[1]); - levelInteger = Integer.parseInt(codecsParts[2]); + profileInteger = Integer.parseInt(parts[1]); + levelInteger = Integer.parseInt(parts[2]); } else { // We don't recognize the format. Log.w(TAG, "Ignoring malformed AVC codec string: " + codec); @@ -524,6 +519,31 @@ public final class MediaCodecUtil { } } + private static @Nullable Pair getAacCodecProfileAndLevel( + String codec, String[] parts) { + if (parts.length != 3) { + Log.w(TAG, "Ignoring malformed MP4A codec string: " + codec); + return null; + } + try { + // Get the object type indication, which is a hexadecimal value (see RFC 6381/ISO 14496-1). + int objectTypeIndication = Integer.parseInt(parts[1], 16); + String mimeType = MimeTypes.getMimeTypeFromMp4ObjectType(objectTypeIndication); + if (MimeTypes.AUDIO_AAC.equals(mimeType)) { + // For MPEG-4 audio this is followed by an audio object type indication as a decimal number. + int audioObjectTypeIndication = Integer.parseInt(parts[2]); + int profile = MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.get(audioObjectTypeIndication, -1); + if (profile != -1) { + // Level is set to zero in AAC decoder CodecProfileLevels. + return new Pair<>(profile, 0); + } + } + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed MP4A codec string: " + codec); + } + return null; + } + private interface MediaCodecListCompat { /** @@ -652,12 +672,41 @@ public final class MediaCodecUtil { } + /** + * Comparator for ordering media codecs that handle {@link MimeTypes#AUDIO_RAW} to work around + * possible inconsistent behavior across different devices. A list sorted with this comparator has + * more preferred codecs first. + */ + private static final class RawAudioCodecComparator implements Comparator { + @Override + public int compare(MediaCodecInfo a, MediaCodecInfo b) { + return scoreMediaCodecInfo(a) - scoreMediaCodecInfo(b); + } + + private static int scoreMediaCodecInfo(MediaCodecInfo mediaCodecInfo) { + String name = mediaCodecInfo.name; + if (name.startsWith("OMX.google") || name.startsWith("c2.android")) { + // Prefer generic decoders over ones provided by the device. + return -1; + } + if (Util.SDK_INT < 26 && name.equals("OMX.MTK.AUDIO.DECODER.RAW")) { + // This decoder may modify the audio, so any other compatible decoders take precedence. See + // [Internal: b/62337687]. + return 1; + } + return 0; + } + } + static { AVC_PROFILE_NUMBER_TO_CONST = new SparseIntArray(); AVC_PROFILE_NUMBER_TO_CONST.put(66, CodecProfileLevel.AVCProfileBaseline); AVC_PROFILE_NUMBER_TO_CONST.put(77, CodecProfileLevel.AVCProfileMain); AVC_PROFILE_NUMBER_TO_CONST.put(88, CodecProfileLevel.AVCProfileExtended); AVC_PROFILE_NUMBER_TO_CONST.put(100, CodecProfileLevel.AVCProfileHigh); + AVC_PROFILE_NUMBER_TO_CONST.put(110, CodecProfileLevel.AVCProfileHigh10); + AVC_PROFILE_NUMBER_TO_CONST.put(122, CodecProfileLevel.AVCProfileHigh422); + AVC_PROFILE_NUMBER_TO_CONST.put(244, CodecProfileLevel.AVCProfileHigh444); AVC_LEVEL_NUMBER_TO_CONST = new SparseIntArray(); AVC_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.AVCLevel1); @@ -706,6 +755,20 @@ public final class MediaCodecUtil { HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H180", CodecProfileLevel.HEVCHighTierLevel6); HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H183", CodecProfileLevel.HEVCHighTierLevel61); HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H186", CodecProfileLevel.HEVCHighTierLevel62); + + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE = new SparseIntArray(); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(1, CodecProfileLevel.AACObjectMain); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(2, CodecProfileLevel.AACObjectLC); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(3, CodecProfileLevel.AACObjectSSR); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(4, CodecProfileLevel.AACObjectLTP); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(5, CodecProfileLevel.AACObjectHE); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(6, CodecProfileLevel.AACObjectScalable); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(17, CodecProfileLevel.AACObjectERLC); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(20, CodecProfileLevel.AACObjectERScalable); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(23, CodecProfileLevel.AACObjectLD); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(29, CodecProfileLevel.AACObjectHE_PS); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(39, CodecProfileLevel.AACObjectELD); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(42, CodecProfileLevel.AACObjectXHE); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java index 9137bad4fd..7e4861a8cb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata; +import android.support.annotation.Nullable; + /** * Decodes metadata from binary data. */ @@ -24,9 +26,8 @@ public interface MetadataDecoder { * Decodes a {@link Metadata} element from the provided input buffer. * * @param inputBuffer The input buffer to decode. - * @return The decoded metadata object. - * @throws MetadataDecoderException If a problem occurred decoding the data. + * @return The decoded metadata object, or null if the metadata could not be decoded. */ - Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException; - + @Nullable + Metadata decode(MetadataInputBuffer inputBuffer); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index 7d36d87a9e..152eb97e0c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -19,12 +19,14 @@ import android.os.Handler; import android.os.Handler.Callback; import android.os.Looper; import android.os.Message; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; /** @@ -46,7 +48,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { private final MetadataDecoderFactory decoderFactory; private final MetadataOutput output; - private final Handler outputHandler; + private final @Nullable Handler outputHandler; private final FormatHolder formatHolder; private final MetadataInputBuffer buffer; private final Metadata[] pendingMetadata; @@ -61,11 +63,11 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { * @param output The output. * @param outputLooper The looper associated with the thread on which the output should be called. * If the output makes use of standard Android UI components, then this should normally be the - * looper associated with the application's main thread, which can be obtained using - * {@link android.app.Activity#getMainLooper()}. Null may be passed if the output should be - * called directly on the player's internal rendering thread. + * looper associated with the application's main thread, which can be obtained using {@link + * android.app.Activity#getMainLooper()}. Null may be passed if the output should be called + * directly on the player's internal rendering thread. */ - public MetadataRenderer(MetadataOutput output, Looper outputLooper) { + public MetadataRenderer(MetadataOutput output, @Nullable Looper outputLooper) { this(output, outputLooper, MetadataDecoderFactory.DEFAULT); } @@ -73,16 +75,17 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { * @param output The output. * @param outputLooper The looper associated with the thread on which the output should be called. * If the output makes use of standard Android UI components, then this should normally be the - * looper associated with the application's main thread, which can be obtained using - * {@link android.app.Activity#getMainLooper()}. Null may be passed if the output should be - * called directly on the player's internal rendering thread. + * looper associated with the application's main thread, which can be obtained using {@link + * android.app.Activity#getMainLooper()}. Null may be passed if the output should be called + * directly on the player's internal rendering thread. * @param decoderFactory A factory from which to obtain {@link MetadataDecoder} instances. */ - public MetadataRenderer(MetadataOutput output, Looper outputLooper, - MetadataDecoderFactory decoderFactory) { + public MetadataRenderer( + MetadataOutput output, @Nullable Looper outputLooper, MetadataDecoderFactory decoderFactory) { super(C.TRACK_TYPE_METADATA); this.output = Assertions.checkNotNull(output); - this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this); + this.outputHandler = + outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this); this.decoderFactory = Assertions.checkNotNull(decoderFactory); formatHolder = new FormatHolder(); buffer = new MetadataInputBuffer(); @@ -125,14 +128,10 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } else { buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; buffer.flip(); - try { - int index = (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT; - pendingMetadata[index] = decoder.decode(buffer); - pendingMetadataTimestamps[index] = buffer.timeUs; - pendingMetadataCount++; - } catch (MetadataDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); - } + int index = (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT; + pendingMetadata[index] = decoder.decode(buffer); + pendingMetadataTimestamps[index] = buffer.timeUs; + pendingMetadataCount++; } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java index 5f521aada6..7d70d9de1c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata.emsg; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; @@ -81,12 +83,12 @@ public final class EventMessage implements Metadata.Entry { } /* package */ EventMessage(Parcel in) { - schemeIdUri = in.readString(); - value = in.readString(); + schemeIdUri = castNonNull(in.readString()); + value = castNonNull(in.readString()); presentationTimeUs = in.readLong(); durationMs = in.readLong(); id = in.readLong(); - messageData = in.createByteArray(); + messageData = castNonNull(in.createByteArray()); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java index ae78f712c7..53976da0d1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata.id3; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; @@ -29,11 +31,12 @@ public final class ApicFrame extends Id3Frame { public static final String ID = "APIC"; public final String mimeType; - public final String description; + public final @Nullable String description; public final int pictureType; public final byte[] pictureData; - public ApicFrame(String mimeType, String description, int pictureType, byte[] pictureData) { + public ApicFrame( + String mimeType, @Nullable String description, int pictureType, byte[] pictureData) { super(ID); this.mimeType = mimeType; this.description = description; @@ -43,10 +46,10 @@ public final class ApicFrame extends Id3Frame { /* package */ ApicFrame(Parcel in) { super(ID); - mimeType = in.readString(); - description = in.readString(); + mimeType = castNonNull(in.readString()); + description = castNonNull(in.readString()); pictureType = in.readInt(); - pictureData = in.createByteArray(); + pictureData = castNonNull(in.createByteArray()); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java index 129803299c..c48829ae54 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata.id3; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; @@ -33,8 +35,8 @@ public final class BinaryFrame extends Id3Frame { } /* package */ BinaryFrame(Parcel in) { - super(in.readString()); - data = in.createByteArray(); + super(castNonNull(in.readString())); + data = castNonNull(in.createByteArray()); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java index aca530cdee..7ffb6d028c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata.id3; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -54,7 +56,7 @@ public final class ChapterFrame extends Id3Frame { /* package */ ChapterFrame(Parcel in) { super(ID); - this.chapterId = in.readString(); + this.chapterId = castNonNull(in.readString()); this.startTimeMs = in.readInt(); this.endTimeMs = in.readInt(); this.startOffset = in.readLong(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java index 56b08bbee3..c4a7c06e49 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata.id3; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Util; @@ -45,7 +47,7 @@ public final class ChapterTocFrame extends Id3Frame { /* package */ ChapterTocFrame(Parcel in) { super(ID); - this.elementId = in.readString(); + this.elementId = castNonNull(in.readString()); this.isRoot = in.readByte() != 0; this.isOrdered = in.readByte() != 0; this.children = in.createStringArray(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java index e84b776790..5666e48939 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata.id3; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; @@ -40,9 +42,9 @@ public final class CommentFrame extends Id3Frame { /* package */ CommentFrame(Parcel in) { super(ID); - language = in.readString(); - description = in.readString(); - text = in.readString(); + language = castNonNull(in.readString()); + description = castNonNull(in.readString()); + text = castNonNull(in.readString()); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java index 8b665fce00..990d8f2e48 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata.id3; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; @@ -43,10 +45,10 @@ public final class GeobFrame extends Id3Frame { /* package */ GeobFrame(Parcel in) { super(ID); - mimeType = in.readString(); - filename = in.readString(); - description = in.readString(); - data = in.createByteArray(); + mimeType = castNonNull(in.readString()); + filename = castNonNull(in.readString()); + description = castNonNull(in.readString()); + data = castNonNull(in.createByteArray()); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index ad24bac6c4..914fca5eef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.metadata.id3; +import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; @@ -88,7 +89,7 @@ public final class Id3Decoder implements MetadataDecoder { private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; private static final int ID3_TEXT_ENCODING_UTF_8 = 3; - private final FramePredicate framePredicate; + private final @Nullable FramePredicate framePredicate; public Id3Decoder() { this(null); @@ -97,12 +98,12 @@ public final class Id3Decoder implements MetadataDecoder { /** * @param framePredicate Determines which frames are decoded. May be null to decode all frames. */ - public Id3Decoder(FramePredicate framePredicate) { + public Id3Decoder(@Nullable FramePredicate framePredicate) { this.framePredicate = framePredicate; } @Override - public Metadata decode(MetadataInputBuffer inputBuffer) { + public @Nullable Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = inputBuffer.data; return decode(buffer.array(), buffer.limit()); } @@ -112,9 +113,10 @@ public final class Id3Decoder implements MetadataDecoder { * * @param data The bytes to decode ID3 tags from. * @param size Amount of bytes in {@code data} to read. - * @return A {@link Metadata} object containing the decoded ID3 tags. + * @return A {@link Metadata} object containing the decoded ID3 tags, or null if the data could + * not be decoded. */ - public Metadata decode(byte[] data, int size) { + public @Nullable Metadata decode(byte[] data, int size) { List id3Frames = new ArrayList<>(); ParsableByteArray id3Data = new ParsableByteArray(data, size); @@ -156,7 +158,7 @@ public final class Id3Decoder implements MetadataDecoder { * @param data A {@link ParsableByteArray} from which the header should be read. * @return The parsed header, or null if the ID3 tag is unsupported. */ - private static Id3Header decodeHeader(ParsableByteArray data) { + private static @Nullable Id3Header decodeHeader(ParsableByteArray data) { if (data.bytesLeft() < ID3_HEADER_LENGTH) { Log.w(TAG, "Data too short to be an ID3 tag"); return null; @@ -270,8 +272,12 @@ public final class Id3Decoder implements MetadataDecoder { } } - private static Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data, - boolean unsignedIntFrameSizeHack, int frameHeaderSize, FramePredicate framePredicate) { + private static @Nullable Id3Frame decodeFrame( + int majorVersion, + ParsableByteArray id3Data, + boolean unsignedIntFrameSizeHack, + int frameHeaderSize, + @Nullable FramePredicate framePredicate) { int frameId0 = id3Data.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte(); @@ -399,8 +405,8 @@ public final class Id3Decoder implements MetadataDecoder { } } - private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) - throws UnsupportedEncodingException { + private static @Nullable TextInformationFrame decodeTxxxFrame( + ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { if (frameSize < 1) { // Frame is malformed. return null; @@ -422,8 +428,8 @@ public final class Id3Decoder implements MetadataDecoder { return new TextInformationFrame("TXXX", description, value); } - private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data, - int frameSize, String id) throws UnsupportedEncodingException { + private static @Nullable TextInformationFrame decodeTextInformationFrame( + ParsableByteArray id3Data, int frameSize, String id) throws UnsupportedEncodingException { if (frameSize < 1) { // Frame is malformed. return null; @@ -441,7 +447,7 @@ public final class Id3Decoder implements MetadataDecoder { return new TextInformationFrame(id, null, value); } - private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize) + private static @Nullable UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { if (frameSize < 1) { // Frame is malformed. @@ -552,7 +558,7 @@ public final class Id3Decoder implements MetadataDecoder { return new ApicFrame(mimeType, description, pictureType, pictureData); } - private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) + private static @Nullable CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { if (frameSize < 4) { // Frame is malformed. @@ -579,9 +585,14 @@ public final class Id3Decoder implements MetadataDecoder { return new CommentFrame(language, description, text); } - private static ChapterFrame decodeChapterFrame(ParsableByteArray id3Data, int frameSize, - int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize, - FramePredicate framePredicate) throws UnsupportedEncodingException { + private static ChapterFrame decodeChapterFrame( + ParsableByteArray id3Data, + int frameSize, + int majorVersion, + boolean unsignedIntFrameSizeHack, + int frameHeaderSize, + @Nullable FramePredicate framePredicate) + throws UnsupportedEncodingException { int framePosition = id3Data.getPosition(); int chapterIdEndIndex = indexOfZeroByte(id3Data.data, framePosition); String chapterId = new String(id3Data.data, framePosition, chapterIdEndIndex - framePosition, @@ -614,9 +625,14 @@ public final class Id3Decoder implements MetadataDecoder { return new ChapterFrame(chapterId, startTime, endTime, startOffset, endOffset, subFrameArray); } - private static ChapterTocFrame decodeChapterTOCFrame(ParsableByteArray id3Data, int frameSize, - int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize, - FramePredicate framePredicate) throws UnsupportedEncodingException { + private static ChapterTocFrame decodeChapterTOCFrame( + ParsableByteArray id3Data, + int frameSize, + int majorVersion, + boolean unsignedIntFrameSizeHack, + int frameHeaderSize, + @Nullable FramePredicate framePredicate) + throws UnsupportedEncodingException { int framePosition = id3Data.getPosition(); int elementIdEndIndex = indexOfZeroByte(id3Data.data, framePosition); String elementId = new String(id3Data.data, framePosition, elementIdEndIndex - framePosition, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java index 433c52bdcc..27ea833deb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.metadata.id3; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.util.Assertions; /** * Base class for ID3 frames. @@ -29,7 +28,7 @@ public abstract class Id3Frame implements Metadata.Entry { public final String id; public Id3Frame(String id) { - this.id = Assertions.checkNotNull(id); + this.id = id; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java new file mode 100644 index 0000000000..c191676ce2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.metadata.id3; + +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.util.Util; + +/** Internal ID3 frame that is intended for use by the player. */ +public final class InternalFrame extends Id3Frame { + + public static final String ID = "----"; + + public final String domain; + public final String description; + public final String text; + + public InternalFrame(String domain, String description, String text) { + super(ID); + this.domain = domain; + this.description = description; + this.text = text; + } + + /* package */ InternalFrame(Parcel in) { + super(ID); + domain = castNonNull(in.readString()); + description = castNonNull(in.readString()); + text = castNonNull(in.readString()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + InternalFrame other = (InternalFrame) obj; + return Util.areEqual(description, other.description) + && Util.areEqual(domain, other.domain) + && Util.areEqual(text, other.text); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (domain != null ? domain.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (text != null ? text.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return id + ": domain=" + domain + ", description=" + description; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(domain); + dest.writeString(text); + } + + public static final Creator CREATOR = + new Creator() { + + @Override + public InternalFrame createFromParcel(Parcel in) { + return new InternalFrame(in); + } + + @Override + public InternalFrame[] newArray(int size) { + return new InternalFrame[size]; + } + }; +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java index 1b5ba67c11..a10ce229d9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata.id3; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; @@ -39,8 +41,8 @@ public final class PrivFrame extends Id3Frame { /* package */ PrivFrame(Parcel in) { super(ID); - owner = in.readString(); - privateData = in.createByteArray(); + owner = castNonNull(in.readString()); + privateData = castNonNull(in.createByteArray()); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java index dbab4ca7a8..62175ee90c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata.id3; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; @@ -25,19 +27,19 @@ import com.google.android.exoplayer2.util.Util; */ public final class TextInformationFrame extends Id3Frame { - public final String description; + public final @Nullable String description; public final String value; - public TextInformationFrame(String id, String description, String value) { + public TextInformationFrame(String id, @Nullable String description, String value) { super(id); this.description = description; this.value = value; } /* package */ TextInformationFrame(Parcel in) { - super(in.readString()); + super(castNonNull(in.readString())); description = in.readString(); - value = in.readString(); + value = castNonNull(in.readString()); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java index f657eefc30..4b35131bea 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata.id3; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; @@ -25,19 +27,19 @@ import com.google.android.exoplayer2.util.Util; */ public final class UrlLinkFrame extends Id3Frame { - public final String description; + public final @Nullable String description; public final String url; - public UrlLinkFrame(String id, String description, String url) { + public UrlLinkFrame(String id, @Nullable String description, String url) { super(id); this.description = description; this.url = url; } /* package */ UrlLinkFrame(Parcel in) { - super(in.readString()); + super(castNonNull(in.readString())); description = in.readString(); - url = in.readString(); + url = castNonNull(in.readString()); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java index 4050daa1cb..d6fc4f6c19 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.metadata.scte35; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; -import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -46,7 +45,7 @@ public final class SpliceInfoDecoder implements MetadataDecoder { } @Override - public Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException { + public Metadata decode(MetadataInputBuffer inputBuffer) { // Internal timestamps adjustment. if (timestampAdjuster == null || inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java index 98360b909c..20b7860784 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.offline; import android.net.Uri; import android.support.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -24,6 +25,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; +import java.util.Collections; +import java.util.List; /** Contains the necessary parameters for a download or remove action. */ public abstract class DownloadAction { @@ -50,6 +53,48 @@ public abstract class DownloadAction { throws IOException; } + private static @Nullable Deserializer[] defaultDeserializers; + + /** Returns available default {@link Deserializer}s. */ + public static synchronized Deserializer[] getDefaultDeserializers() { + if (defaultDeserializers != null) { + return defaultDeserializers; + } + Deserializer[] deserializers = new Deserializer[4]; + int count = 0; + deserializers[count++] = ProgressiveDownloadAction.DESERIALIZER; + Class clazz; + // Full class names used for constructor args so the LINT rule triggers if any of them move. + try { + // LINT.IfChange + clazz = Class.forName("com.google.android.exoplayer2.source.dash.offline.DashDownloadAction"); + // LINT.ThenChange(../../../../../../../../../dash/proguard-rules.txt) + deserializers[count++] = getDeserializer(clazz); + } catch (Exception e) { + // Do nothing. + } + try { + // LINT.IfChange + clazz = Class.forName("com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction"); + // LINT.ThenChange(../../../../../../../../../hls/proguard-rules.txt) + deserializers[count++] = getDeserializer(clazz); + } catch (Exception e) { + // Do nothing. + } + try { + // LINT.IfChange + clazz = + Class.forName( + "com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction"); + // LINT.ThenChange(../../../../../../../../../smoothstreaming/proguard-rules.txt) + deserializers[count++] = getDeserializer(clazz); + } catch (Exception e) { + // Do nothing. + } + defaultDeserializers = Arrays.copyOf(Assertions.checkNotNull(deserializers), count); + return defaultDeserializers; + } + /** * Deserializes one action that was serialized with {@link #serializeToStream(DownloadAction, * OutputStream)} from the {@code input}, using the {@link Deserializer}s that supports the @@ -132,11 +177,16 @@ public abstract class DownloadAction { return uri.equals(other.uri); } + /** Returns keys of tracks to be downloaded. */ + public List getKeys() { + return Collections.emptyList(); + } + /** Serializes itself into the {@code output}. */ protected abstract void writeToStream(DataOutputStream output) throws IOException; /** Creates a {@link Downloader} with the given parameters. */ - protected abstract Downloader createDownloader( + public abstract Downloader createDownloader( DownloaderConstructorHelper downloaderConstructorHelper); @Override @@ -160,4 +210,9 @@ public abstract class DownloadAction { return result; } + private static Deserializer getDeserializer(Class clazz) + throws NoSuchFieldException, IllegalAccessException { + Object value = clazz.getDeclaredField("DESERIALIZER").get(null); + return (Deserializer) Assertions.checkNotNull(value); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index 0e2c5874b1..3b825bb14a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -108,7 +108,8 @@ public final class DownloadManager { * @param upstreamDataSourceFactory A {@link DataSource.Factory} for creating data sources for * downloading upstream data. * @param actionSaveFile File to save active actions. - * @param deserializers Used to deserialize {@link DownloadAction}s. + * @param deserializers Used to deserialize {@link DownloadAction}s. If empty, {@link + * DownloadAction#getDefaultDeserializers()} is used instead. */ public DownloadManager( Cache cache, @@ -127,7 +128,8 @@ public final class DownloadManager { * @param constructorHelper A {@link DownloaderConstructorHelper} to create {@link Downloader}s * for downloading data. * @param actionFile The file in which active actions are saved. - * @param deserializers Used to deserialize {@link DownloadAction}s. + * @param deserializers Used to deserialize {@link DownloadAction}s. If empty, {@link + * DownloadAction#getDefaultDeserializers()} is used instead. */ public DownloadManager( DownloaderConstructorHelper constructorHelper, @@ -149,7 +151,8 @@ public final class DownloadManager { * @param maxSimultaneousDownloads The maximum number of simultaneous download tasks. * @param minRetryCount The minimum number of times a task must be retried before failing. * @param actionFile The file in which active actions are saved. - * @param deserializers Used to deserialize {@link DownloadAction}s. + * @param deserializers Used to deserialize {@link DownloadAction}s. If empty, {@link + * DownloadAction#getDefaultDeserializers()} is used instead. */ public DownloadManager( DownloaderConstructorHelper constructorHelper, @@ -157,13 +160,12 @@ public final class DownloadManager { int minRetryCount, File actionFile, Deserializer... deserializers) { - Assertions.checkArgument(deserializers.length > 0, "At least one Deserializer is required."); - this.downloaderConstructorHelper = constructorHelper; this.maxActiveDownloadTasks = maxSimultaneousDownloads; this.minRetryCount = minRetryCount; this.actionFile = new ActionFile(actionFile); - this.deserializers = deserializers; + this.deserializers = + deserializers.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers(); this.downloadsStopped = true; tasks = new ArrayList<>(); @@ -262,12 +264,23 @@ public final class DownloadManager { return task.id; } - /** Returns the current number of tasks. */ + /** Returns the number of tasks. */ public int getTaskCount() { Assertions.checkState(!released); return tasks.size(); } + /** Returns the number of download tasks. */ + public int getDownloadCount() { + int count = 0; + for (int i = 0; i < tasks.size(); i++) { + if (!tasks.get(i).action.isRemoveAction) { + count++; + } + } + return count; + } + /** Returns the state of a task, or null if no such task exists */ public @Nullable TaskState getTaskState(int taskId) { Assertions.checkState(!released); @@ -717,6 +730,11 @@ public final class DownloadManager { return "CANCELING"; case STATE_STARTED_STOPPING: return "STOPPING"; + case STATE_QUEUED: + case STATE_STARTED: + case STATE_COMPLETED: + case STATE_CANCELED: + case STATE_FAILED: default: return TaskState.getStateString(currentState); } @@ -729,6 +747,11 @@ public final class DownloadManager { case STATE_STARTED_CANCELING: case STATE_STARTED_STOPPING: return STATE_STARTED; + case STATE_QUEUED: + case STATE_STARTED: + case STATE_COMPLETED: + case STATE_CANCELED: + case STATE_FAILED: default: return currentState; } @@ -762,7 +785,7 @@ public final class DownloadManager { private void stop() { if (changeStateAndNotify(STATE_STARTED, STATE_STARTED_STOPPING)) { logd("Stopping", this); - thread.interrupt(); + cancelDownload(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index 908aae481a..f7ca793b22 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -44,21 +44,20 @@ public abstract class DownloadService extends Service { /** Starts a download service, adding a new {@link DownloadAction} to be executed. */ public static final String ACTION_ADD = "com.google.android.exoplayer.downloadService.action.ADD"; + /** Reloads the download requirements. */ + public static final String ACTION_RELOAD_REQUIREMENTS = + "com.google.android.exoplayer.downloadService.action.RELOAD_REQUIREMENTS"; + /** Like {@link #ACTION_INIT}, but with {@link #KEY_FOREGROUND} implicitly set to true. */ private static final String ACTION_RESTART = "com.google.android.exoplayer.downloadService.action.RESTART"; - /** Starts download tasks. */ - private static final String ACTION_START_DOWNLOADS = - "com.google.android.exoplayer.downloadService.action.START_DOWNLOADS"; - - /** Stops download tasks. */ - private static final String ACTION_STOP_DOWNLOADS = - "com.google.android.exoplayer.downloadService.action.STOP_DOWNLOADS"; - /** Key for the {@link DownloadAction} in an {@link #ACTION_ADD} intent. */ public static final String KEY_DOWNLOAD_ACTION = "download_action"; + /** Invalid foreground notification id which can be used to run the service in the background. */ + public static final int FOREGROUND_NOTIFICATION_ID_NONE = 0; + /** * Key for a boolean flag in any intent to indicate whether the service was started in the * foreground. If set, the service is guaranteed to call {@link #startForeground(int, @@ -77,8 +76,10 @@ public abstract class DownloadService extends Service { // tasks the resume more quickly than when relying on the scheduler alone. private static final HashMap, RequirementsHelper> requirementsHelpers = new HashMap<>(); + private static final Requirements DEFAULT_REQUIREMENTS = + new Requirements(Requirements.NETWORK_TYPE_ANY, false, false); - private final ForegroundNotificationUpdater foregroundNotificationUpdater; + private final @Nullable ForegroundNotificationUpdater foregroundNotificationUpdater; private final @Nullable String channelId; private final @StringRes int channelName; @@ -86,18 +87,31 @@ public abstract class DownloadService extends Service { private DownloadManagerListener downloadManagerListener; private int lastStartId; private boolean startedInForeground; + private boolean taskRemoved; /** - * Creates a DownloadService with {@link #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}. + * Creates a DownloadService. * - * @param foregroundNotificationId The notification id for the foreground notification, must not - * be 0. + *

    If {@code foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE} (value + * {@value #FOREGROUND_NOTIFICATION_ID_NONE}) then the service runs in the background. No + * foreground notification is displayed and {@link #getScheduler()} isn't called. + * + *

    If {@code foregroundNotificationId} isn't {@link #FOREGROUND_NOTIFICATION_ID_NONE} (value + * {@value #FOREGROUND_NOTIFICATION_ID_NONE}) the service runs in the foreground with {@link + * #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}. In that case {@link + * #getForegroundNotification(TaskState[])} should be overridden in the subclass. + * + * @param foregroundNotificationId The notification id for the foreground notification, or {@link + * #FOREGROUND_NOTIFICATION_ID_NONE} (value {@value #FOREGROUND_NOTIFICATION_ID_NONE}) */ protected DownloadService(int foregroundNotificationId) { this(foregroundNotificationId, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL); } /** + * Creates a DownloadService which will run in the foreground. {@link + * #getForegroundNotification(TaskState[])} should be overridden in the subclass. + * * @param foregroundNotificationId The notification id for the foreground notification, must not * be 0. * @param foregroundNotificationUpdateInterval The maximum interval to update foreground @@ -113,6 +127,9 @@ public abstract class DownloadService extends Service { } /** + * Creates a DownloadService which will run in the foreground. {@link + * #getForegroundNotification(TaskState[])} should be overridden in the subclass. + * * @param foregroundNotificationId The notification id for the foreground notification. Must not * be 0. * @param foregroundNotificationUpdateInterval The maximum interval between updates to the @@ -130,8 +147,10 @@ public abstract class DownloadService extends Service { @Nullable String channelId, @StringRes int channelName) { foregroundNotificationUpdater = - new ForegroundNotificationUpdater( - foregroundNotificationId, foregroundNotificationUpdateInterval); + foregroundNotificationId == 0 + ? null + : new ForegroundNotificationUpdater( + foregroundNotificationId, foregroundNotificationUpdateInterval); this.channelId = channelId; this.channelName = channelName; } @@ -150,8 +169,7 @@ public abstract class DownloadService extends Service { Class clazz, DownloadAction downloadAction, boolean foreground) { - return new Intent(context, clazz) - .setAction(ACTION_ADD) + return getIntent(context, clazz, ACTION_ADD) .putExtra(KEY_DOWNLOAD_ACTION, downloadAction.toByteArray()) .putExtra(KEY_FOREGROUND, foreground); } @@ -160,9 +178,9 @@ public abstract class DownloadService extends Service { * Starts the service, adding an action to be executed. * * @param context A {@link Context}. - * @param clazz The concrete download service being targeted by the intent. + * @param clazz The concrete download service to be started. * @param downloadAction The action to be executed. - * @param foreground Whether this intent will be used to start the service in the foreground. + * @param foreground Whether the service is started in the foreground. */ public static void startWithAction( Context context, @@ -177,6 +195,32 @@ public abstract class DownloadService extends Service { } } + /** + * Starts the service without adding a new action. If there are any not finished actions and the + * requirements are met, the service resumes executing actions. Otherwise it stops immediately. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @see #startForeground(Context, Class) + */ + public static void start(Context context, Class clazz) { + context.startService(getIntent(context, clazz, ACTION_INIT)); + } + + /** + * Starts the service in the foreground without adding a new action. If there are any not finished + * actions and the requirements are met, the service resumes executing actions. Otherwise it stops + * immediately. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @see #start(Context, Class) + */ + public static void startForeground(Context context, Class clazz) { + Intent intent = getIntent(context, clazz, ACTION_INIT).putExtra(KEY_FOREGROUND, true); + Util.startForegroundService(context, intent); + } + @Override public void onCreate() { logd("onCreate"); @@ -187,33 +231,27 @@ public abstract class DownloadService extends Service { downloadManager = getDownloadManager(); downloadManagerListener = new DownloadManagerListener(); downloadManager.addListener(downloadManagerListener); - - RequirementsHelper requirementsHelper; - synchronized (requirementsHelpers) { - Class clazz = getClass(); - requirementsHelper = requirementsHelpers.get(clazz); - if (requirementsHelper == null) { - requirementsHelper = new RequirementsHelper(this, getRequirements(), getScheduler(), clazz); - requirementsHelpers.put(clazz, requirementsHelper); - } - } - requirementsHelper.start(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { lastStartId = startId; + taskRemoved = false; String intentAction = null; if (intent != null) { intentAction = intent.getAction(); startedInForeground |= intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction); } + // intentAction is null if the service is restarted or no action is specified. + if (intentAction == null) { + intentAction = ACTION_INIT; + } logd("onStartCommand action: " + intentAction + " startId: " + startId); switch (intentAction) { case ACTION_INIT: case ACTION_RESTART: - // Do nothing. The RequirementsWatcher will start downloads when possible. + // Do nothing. break; case ACTION_ADD: byte[] actionData = intent.getByteArrayExtra(KEY_DOWNLOAD_ACTION); @@ -227,35 +265,42 @@ public abstract class DownloadService extends Service { } } break; - case ACTION_STOP_DOWNLOADS: - downloadManager.stopDownloads(); - break; - case ACTION_START_DOWNLOADS: - downloadManager.startDownloads(); + case ACTION_RELOAD_REQUIREMENTS: + stopWatchingRequirements(); break; default: Log.e(TAG, "Ignoring unrecognized action: " + intentAction); break; } + + Requirements requirements = getRequirements(); + if (requirements.checkRequirements(this)) { + downloadManager.startDownloads(); + } else { + downloadManager.stopDownloads(); + } + maybeStartWatchingRequirements(requirements); + if (downloadManager.isIdle()) { stop(); } return START_STICKY; } + @Override + public void onTaskRemoved(Intent rootIntent) { + logd("onTaskRemoved rootIntent: " + rootIntent); + taskRemoved = true; + } + @Override public void onDestroy() { logd("onDestroy"); - foregroundNotificationUpdater.stopPeriodicUpdates(); - downloadManager.removeListener(downloadManagerListener); - if (downloadManager.getTaskCount() == 0) { - synchronized (requirementsHelpers) { - RequirementsHelper requirementsHelper = requirementsHelpers.remove(getClass()); - if (requirementsHelper != null) { - requirementsHelper.stop(); - } - } + if (foregroundNotificationUpdater != null) { + foregroundNotificationUpdater.stopPeriodicUpdates(); } + downloadManager.removeListener(downloadManagerListener); + maybeStopWatchingRequirements(); } @Nullable @@ -284,11 +329,13 @@ public abstract class DownloadService extends Service { * device has network connectivity. */ protected Requirements getRequirements() { - return new Requirements(Requirements.NETWORK_TYPE_ANY, false, false); + return DEFAULT_REQUIREMENTS; } /** - * Returns a notification to be displayed when this service running in the foreground. + * Should be overridden in the subclass if the service will be run in the foreground. + * + *

    Returns a notification to be displayed when this service running in the foreground. * *

    This method is called when there is a task state change and periodically while there are * active tasks. The periodic update interval can be set using {@link #DownloadService(int, @@ -301,7 +348,11 @@ public abstract class DownloadService extends Service { * @param taskStates The states of all current tasks. * @return The foreground notification to display. */ - protected abstract Notification getForegroundNotification(TaskState[] taskStates); + protected Notification getForegroundNotification(TaskState[] taskStates) { + throw new IllegalStateException( + getClass().getName() + + " is started in the foreground but getForegroundNotification() is not implemented."); + } /** * Called when the state of a task changes. @@ -312,14 +363,50 @@ public abstract class DownloadService extends Service { // Do nothing. } - private void stop() { - foregroundNotificationUpdater.stopPeriodicUpdates(); - // Make sure startForeground is called before stopping. Workaround for [Internal: b/69424260]. - if (startedInForeground && Util.SDK_INT >= 26) { - foregroundNotificationUpdater.showNotificationIfNotAlready(); + private void maybeStartWatchingRequirements(Requirements requirements) { + if (downloadManager.getDownloadCount() == 0) { + return; + } + Class clazz = getClass(); + RequirementsHelper requirementsHelper = requirementsHelpers.get(clazz); + if (requirementsHelper == null) { + requirementsHelper = new RequirementsHelper(this, requirements, getScheduler(), clazz); + requirementsHelpers.put(clazz, requirementsHelper); + requirementsHelper.start(); + logd("started watching requirements"); + } + } + + private void maybeStopWatchingRequirements() { + if (downloadManager.getDownloadCount() > 0) { + return; + } + stopWatchingRequirements(); + } + + private void stopWatchingRequirements() { + RequirementsHelper requirementsHelper = requirementsHelpers.remove(getClass()); + if (requirementsHelper != null) { + requirementsHelper.stop(); + logd("stopped watching requirements"); + } + } + + private void stop() { + if (foregroundNotificationUpdater != null) { + foregroundNotificationUpdater.stopPeriodicUpdates(); + // Make sure startForeground is called before stopping. Workaround for [Internal: b/69424260]. + if (startedInForeground && Util.SDK_INT >= 26) { + foregroundNotificationUpdater.showNotificationIfNotAlready(); + } + } + if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644]. + stopSelf(); + logd("stopSelf()"); + } else { + boolean stopSelfResult = stopSelfResult(lastStartId); + logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult); } - boolean stopSelfResult = stopSelfResult(lastStartId); - logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult); } private void logd(String message) { @@ -328,19 +415,26 @@ public abstract class DownloadService extends Service { } } + private static Intent getIntent( + Context context, Class clazz, String action) { + return new Intent(context, clazz).setAction(action); + } + private final class DownloadManagerListener implements DownloadManager.Listener { @Override public void onInitialized(DownloadManager downloadManager) { - // Do nothing. + maybeStartWatchingRequirements(getRequirements()); } @Override public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) { DownloadService.this.onTaskStateChanged(taskState); - if (taskState.state == TaskState.STATE_STARTED) { - foregroundNotificationUpdater.startPeriodicUpdates(); - } else { - foregroundNotificationUpdater.update(); + if (foregroundNotificationUpdater != null) { + if (taskState.state == TaskState.STATE_STARTED) { + foregroundNotificationUpdater.startPeriodicUpdates(); + } else { + foregroundNotificationUpdater.update(); + } } } @@ -430,7 +524,12 @@ public abstract class DownloadService extends Service { @Override public void requirementsMet(RequirementsWatcher requirementsWatcher) { - startServiceWithAction(DownloadService.ACTION_START_DOWNLOADS); + try { + notifyService(); + } catch (Exception e) { + /* If we can't notify the service, don't stop the scheduler. */ + return; + } if (scheduler != null) { scheduler.cancel(); } @@ -438,7 +537,11 @@ public abstract class DownloadService extends Service { @Override public void requirementsNotMet(RequirementsWatcher requirementsWatcher) { - startServiceWithAction(DownloadService.ACTION_STOP_DOWNLOADS); + try { + notifyService(); + } catch (Exception e) { + /* Do nothing. The service isn't running anyway. */ + } if (scheduler != null) { String servicePackage = context.getPackageName(); boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART); @@ -448,10 +551,14 @@ public abstract class DownloadService extends Service { } } - private void startServiceWithAction(String action) { - Intent intent = - new Intent(context, serviceClass).setAction(action).putExtra(KEY_FOREGROUND, true); - Util.startForegroundService(context, intent); + private void notifyService() throws Exception { + Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT); + try { + context.startService(intent); + } catch (IllegalStateException e) { + /* startService will fail if the app is in the background and the service isn't running. */ + throw new Exception(e); + } } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/FilterableManifest.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilterableManifest.java index 35d05fd43b..e688b7216f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/FilterableManifest.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilterableManifest.java @@ -22,9 +22,8 @@ import java.util.List; * keys. * * @param The manifest type. - * @param The stream key type. */ -public interface FilterableManifest { +public interface FilterableManifest { /** * Returns a copy of the manifest including only the streams specified by the given keys. If the @@ -33,5 +32,5 @@ public interface FilterableManifest { * @param streamKeys A non-empty list of stream keys. * @return The filtered manifest. */ - T copy(List streamKeys); + T copy(List streamKeys); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java index 8fec07552b..c32cdf7126 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java @@ -21,25 +21,24 @@ import java.io.IOException; import java.io.InputStream; import java.util.List; -/** A manifest parser that includes only the tracks identified by the given track keys. */ -public final class FilteringManifestParser, K> - implements Parser { +/** A manifest parser that includes only the streams identified by the given stream keys. */ +public final class FilteringManifestParser> implements Parser { private final Parser parser; - private final List trackKeys; + private final List streamKeys; /** * @param parser A parser for the manifest that will be filtered. - * @param trackKeys The track keys. If null or empty then filtering will not occur. + * @param streamKeys The stream keys. If null or empty then filtering will not occur. */ - public FilteringManifestParser(Parser parser, List trackKeys) { + public FilteringManifestParser(Parser parser, List streamKeys) { this.parser = parser; - this.trackKeys = trackKeys; + this.streamKeys = streamKeys; } @Override public T parse(Uri uri, InputStream inputStream) throws IOException { T manifest = parser.parse(uri, inputStream); - return trackKeys == null || trackKeys.isEmpty() ? manifest : manifest.copy(trackKeys); + return streamKeys == null || streamKeys.isEmpty() ? manifest : manifest.copy(streamKeys); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java index d8db6f96c2..7ced2fa41b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java @@ -44,7 +44,33 @@ public final class ProgressiveDownloadAction extends DownloadAction { } }; - public final @Nullable String customCacheKey; + private final @Nullable String customCacheKey; + + /** + * Creates a progressive stream download action. + * + * @param uri Uri of the data to be downloaded. + * @param data Optional custom data for this action. + * @param customCacheKey A custom key that uniquely identifies the original stream. If not null it + * is used for cache indexing. + */ + public static ProgressiveDownloadAction createDownloadAction( + Uri uri, @Nullable byte[] data, @Nullable String customCacheKey) { + return new ProgressiveDownloadAction(uri, /* isRemoveAction= */ false, data, customCacheKey); + } + + /** + * Creates a progressive stream remove action. + * + * @param uri Uri of the data to be removed. + * @param data Optional custom data for this action. + * @param customCacheKey A custom key that uniquely identifies the original stream. If not null it + * is used for cache indexing. + */ + public static ProgressiveDownloadAction createRemoveAction( + Uri uri, @Nullable byte[] data, @Nullable String customCacheKey) { + return new ProgressiveDownloadAction(uri, /* isRemoveAction= */ true, data, customCacheKey); + } /** * @param uri Uri of the data to be downloaded. @@ -52,7 +78,10 @@ public final class ProgressiveDownloadAction extends DownloadAction { * @param data Optional custom data for this action. * @param customCacheKey A custom key that uniquely identifies the original stream. If not null it * is used for cache indexing. + * @deprecated Use {@link #createDownloadAction(Uri, byte[], String)} or {@link + * #createRemoveAction(Uri, byte[], String)}. */ + @Deprecated public ProgressiveDownloadAction( Uri uri, boolean isRemoveAction, @Nullable byte[] data, @Nullable String customCacheKey) { super(TYPE, VERSION, uri, isRemoveAction, data); @@ -60,7 +89,7 @@ public final class ProgressiveDownloadAction extends DownloadAction { } @Override - protected ProgressiveDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { + public ProgressiveDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { return new ProgressiveDownloader(uri, customCacheKey, constructorHelper); } @@ -105,4 +134,5 @@ public final class ProgressiveDownloadAction extends DownloadAction { private String getCacheKey() { return customCacheKey != null ? customCacheKey : CacheUtil.generateKey(uri); } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java index 49b7e36ea6..473209803a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java @@ -51,12 +51,13 @@ public final class ProgressiveDownloadHelper extends DownloadHelper { } @Override - public DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { - return new ProgressiveDownloadAction(uri, false, data, customCacheKey); + public ProgressiveDownloadAction getDownloadAction( + @Nullable byte[] data, List trackKeys) { + return ProgressiveDownloadAction.createDownloadAction(uri, data, customCacheKey); } @Override - public DownloadAction getRemoveAction(@Nullable byte[] data) { - return new ProgressiveDownloadAction(uri, true, data, customCacheKey); + public ProgressiveDownloadAction getRemoveAction(@Nullable byte[] data) { + return ProgressiveDownloadAction.createRemoveAction(uri, data, customCacheKey); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java index cf64d26bb5..8c80a23d67 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.offline; +package com.google.android.exoplayer2.offline; import android.net.Uri; import com.google.android.exoplayer2.C; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java index ae57131641..403b4e797b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java @@ -25,19 +25,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -/** - * {@link DownloadAction} for {@link SegmentDownloader}s. - * - * @param The type of the representation key object. - */ -public abstract class SegmentDownloadAction> extends DownloadAction { +/** {@link DownloadAction} for {@link SegmentDownloader}s. */ +public abstract class SegmentDownloadAction extends DownloadAction { - /** - * Base class for {@link SegmentDownloadAction} {@link Deserializer}s. - * - * @param The type of the representation key object. - */ - protected abstract static class SegmentDownloadActionDeserializer extends Deserializer { + /** Base class for {@link SegmentDownloadAction} {@link Deserializer}s. */ + protected abstract static class SegmentDownloadActionDeserializer extends Deserializer { public SegmentDownloadActionDeserializer(String type, int version) { super(type, version); @@ -52,22 +44,27 @@ public abstract class SegmentDownloadAction> extends Dow byte[] data = new byte[dataLength]; input.readFully(data); int keyCount = input.readInt(); - List keys = new ArrayList<>(); + List keys = new ArrayList<>(); for (int i = 0; i < keyCount; i++) { - keys.add(readKey(input)); + keys.add(readKey(version, input)); } return createDownloadAction(uri, isRemoveAction, data, keys); } /** Deserializes a key from the {@code input}. */ - protected abstract K readKey(DataInputStream input) throws IOException; + protected StreamKey readKey(int version, DataInputStream input) throws IOException { + int periodIndex = input.readInt(); + int groupIndex = input.readInt(); + int trackIndex = input.readInt(); + return new StreamKey(periodIndex, groupIndex, trackIndex); + } /** Returns a {@link DownloadAction}. */ protected abstract DownloadAction createDownloadAction( - Uri manifestUri, boolean isRemoveAction, byte[] data, List keys); + Uri manifestUri, boolean isRemoveAction, byte[] data, List keys); } - public final List keys; + public final List keys; /** * @param type The type of the action. @@ -84,18 +81,23 @@ public abstract class SegmentDownloadAction> extends Dow Uri uri, boolean isRemoveAction, @Nullable byte[] data, - List keys) { + List keys) { super(type, version, uri, isRemoveAction, data); if (isRemoveAction) { Assertions.checkArgument(keys.isEmpty()); this.keys = Collections.emptyList(); } else { - ArrayList mutableKeys = new ArrayList<>(keys); + ArrayList mutableKeys = new ArrayList<>(keys); Collections.sort(mutableKeys); this.keys = Collections.unmodifiableList(mutableKeys); } } + @Override + public List getKeys() { + return keys; + } + @Override public final void writeToStream(DataOutputStream output) throws IOException { output.writeUTF(uri.toString()); @@ -108,9 +110,6 @@ public abstract class SegmentDownloadAction> extends Dow } } - /** Serializes the {@code key} into the {@code output}. */ - protected abstract void writeKey(DataOutputStream output, K key) throws IOException; - @Override public boolean equals(@Nullable Object o) { if (this == o) { @@ -119,7 +118,7 @@ public abstract class SegmentDownloadAction> extends Dow if (!super.equals(o)) { return false; } - SegmentDownloadAction that = (SegmentDownloadAction) o; + SegmentDownloadAction that = (SegmentDownloadAction) o; return keys.equals(that.keys); } @@ -130,4 +129,10 @@ public abstract class SegmentDownloadAction> extends Dow return result; } + /** Serializes the {@code key} into the {@code output}. */ + private void writeKey(DataOutputStream output, StreamKey key) throws IOException { + output.writeInt(key.periodIndex); + output.writeInt(key.groupIndex); + output.writeInt(key.trackIndex); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java index 6ce2121acd..9aa7afd7cd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -35,10 +35,8 @@ import java.util.concurrent.atomic.AtomicBoolean; * Base class for multi segment stream downloaders. * * @param The type of the manifest object. - * @param The type of the streams key object. */ -public abstract class SegmentDownloader, K> - implements Downloader { +public abstract class SegmentDownloader> implements Downloader { /** Smallest unit of content to be downloaded. */ protected static class Segment implements Comparable { @@ -68,7 +66,7 @@ public abstract class SegmentDownloader, K> private final Cache cache; private final CacheDataSource dataSource; private final CacheDataSource offlineDataSource; - private final ArrayList streamKeys; + private final ArrayList streamKeys; private final AtomicBoolean isCanceled; private volatile int totalSegments; @@ -82,7 +80,7 @@ public abstract class SegmentDownloader, K> * @param constructorHelper A {@link DownloaderConstructorHelper} instance. */ public SegmentDownloader( - Uri manifestUri, List streamKeys, DownloaderConstructorHelper constructorHelper) { + Uri manifestUri, List streamKeys, DownloaderConstructorHelper constructorHelper) { this.manifestUri = manifestUri; this.streamKeys = new ArrayList<>(streamKeys); this.cache = constructorHelper.getCache(); @@ -201,6 +199,8 @@ public abstract class SegmentDownloader, K> throws InterruptedException, IOException; /** Initializes the download, returning a list of {@link Segment}s that need to be downloaded. */ + // Writes to downloadedSegments and downloadedBytes are safe. See the comment on download(). + @SuppressWarnings("NonAtomicVolatileUpdate") private List initDownload() throws IOException, InterruptedException { M manifest = getManifest(dataSource, manifestUri); if (!streamKeys.isEmpty()) { diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/StreamKey.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java similarity index 50% rename from library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/StreamKey.java rename to library/core/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java index 6667a3df27..838073cd99 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/StreamKey.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,25 +13,46 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.source.smoothstreaming.manifest; +package com.google.android.exoplayer2.offline; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -/** Uniquely identifies a track in a {@link SsManifest}. */ +/** + * Identifies a given track by the index of the containing period, the index of the containing group + * within the period, and the index of the track within the group. + */ public final class StreamKey implements Comparable { - public final int streamElementIndex; + /** The period index. */ + public final int periodIndex; + /** The group index. */ + public final int groupIndex; + /** The track index. */ public final int trackIndex; - public StreamKey(int streamElementIndex, int trackIndex) { - this.streamElementIndex = streamElementIndex; + /** + * @param groupIndex The group index. + * @param trackIndex The track index. + */ + public StreamKey(int groupIndex, int trackIndex) { + this(0, groupIndex, trackIndex); + } + + /** + * @param periodIndex The period index. + * @param groupIndex The group index. + * @param trackIndex The track index. + */ + public StreamKey(int periodIndex, int groupIndex, int trackIndex) { + this.periodIndex = periodIndex; + this.groupIndex = groupIndex; this.trackIndex = trackIndex; } @Override public String toString() { - return streamElementIndex + "." + trackIndex; + return periodIndex + "." + groupIndex + "." + trackIndex; } @Override @@ -44,12 +65,15 @@ public final class StreamKey implements Comparable { } StreamKey that = (StreamKey) o; - return streamElementIndex == that.streamElementIndex && trackIndex == that.trackIndex; + return periodIndex == that.periodIndex + && groupIndex == that.groupIndex + && trackIndex == that.trackIndex; } @Override public int hashCode() { - int result = streamElementIndex; + int result = periodIndex; + result = 31 * result + groupIndex; result = 31 * result + trackIndex; return result; } @@ -58,9 +82,12 @@ public final class StreamKey implements Comparable { @Override public int compareTo(@NonNull StreamKey o) { - int result = streamElementIndex - o.streamElementIndex; + int result = periodIndex - o.periodIndex; if (result == 0) { - result = trackIndex - o.trackIndex; + result = groupIndex - o.groupIndex; + if (result == 0) { + result = trackIndex - o.trackIndex; + } } return result; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java index 46aa55f094..acd88182f9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java @@ -87,7 +87,7 @@ public final class RequirementsWatcher { public void start() { Assertions.checkNotNull(Looper.myLooper()); - checkRequirements(true); + requirementsWereMet = requirements.checkRequirements(context); IntentFilter filter = new IntentFilter(); if (requirements.getRequiredNetworkType() != Requirements.NETWORK_TYPE_NONE) { @@ -158,13 +158,11 @@ public final class RequirementsWatcher { } } - private void checkRequirements(boolean force) { + private void checkRequirements() { boolean requirementsAreMet = requirements.checkRequirements(context); - if (!force) { - if (requirementsAreMet == requirementsWereMet) { - logd("requirementsAreMet is still " + requirementsAreMet); - return; - } + if (requirementsAreMet == requirementsWereMet) { + logd("requirementsAreMet is still " + requirementsAreMet); + return; } requirementsWereMet = requirementsAreMet; if (requirementsAreMet) { @@ -187,7 +185,7 @@ public final class RequirementsWatcher { public void onReceive(Context context, Intent intent) { if (!isInitialStickyBroadcast()) { logd(RequirementsWatcher.this + " received " + intent.getAction()); - checkRequirements(false); + checkRequirements(); } } } @@ -198,14 +196,14 @@ public final class RequirementsWatcher { public void onAvailable(Network network) { super.onAvailable(network); logd(RequirementsWatcher.this + " NetworkCallback.onAvailable"); - checkRequirements(false); + checkRequirements(); } @Override public void onLost(Network network) { super.onLost(network); logd(RequirementsWatcher.this + " NetworkCallback.onLost"); - checkRequirements(false); + checkRequirements(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java index 8663b4c05c..305a249e4a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -168,6 +168,19 @@ import com.google.android.exoplayer2.Timeline; return window; } + @Override + public final Period getPeriodByUid(Object uid, Period period) { + Pair childUidAndPeriodUid = (Pair) uid; + Object childUid = childUidAndPeriodUid.first; + Object periodUid = childUidAndPeriodUid.second; + int childIndex = getChildIndexByChildUid(childUid); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + getTimelineByChildIndex(childIndex).getPeriodByUid(periodUid, period); + period.windowIndex += firstWindowIndexInChild; + period.uid = uid; + return period; + } + @Override public final Period getPeriod(int periodIndex, Period period, boolean setIds) { int childIndex = getChildIndexByPeriodIndex(periodIndex); @@ -199,6 +212,15 @@ import com.google.android.exoplayer2.Timeline; : getFirstPeriodIndexByChildIndex(childIndex) + periodIndexInChild; } + @Override + public final Object getUidOfPeriod(int periodIndex) { + int childIndex = getChildIndexByPeriodIndex(periodIndex); + int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex); + Object periodUidInChild = + getTimelineByChildIndex(childIndex).getUidOfPeriod(periodIndex - firstPeriodIndexInChild); + return Pair.create(getChildUidByChildIndex(childIndex), periodUidInChild); + } + /** * Returns the index of the child timeline containing the given period index. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java index 32526361f5..2feac2978e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java @@ -19,6 +19,7 @@ import android.os.Handler; import android.support.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import java.util.ArrayList; @@ -34,9 +35,9 @@ public abstract class BaseMediaSource implements MediaSource { private final ArrayList sourceInfoListeners; private final MediaSourceEventListener.EventDispatcher eventDispatcher; - private ExoPlayer player; - private Timeline timeline; - private Object manifest; + private @Nullable ExoPlayer player; + private @Nullable Timeline timeline; + private @Nullable Object manifest; public BaseMediaSource() { sourceInfoListeners = new ArrayList<>(/* initialCapacity= */ 1); @@ -51,12 +52,17 @@ public abstract class BaseMediaSource implements MediaSource { * @param isTopLevelSource Whether this source has been passed directly to {@link * ExoPlayer#prepare(MediaSource)} or {@link ExoPlayer#prepare(MediaSource, boolean, * boolean)}. + * @param mediaTransferListener The transfer listener which should be informed of any media data + * transfers. May be null if no listener is available. Note that this listener should usually + * be only informed of transfers related to the media loads and not of auxiliary loads for + * manifests and other data. */ - protected abstract void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource); + protected abstract void prepareSourceInternal( + ExoPlayer player, boolean isTopLevelSource, @Nullable TransferListener mediaTransferListener); /** * Releases the source. This method is called exactly once after each call to {@link - * #prepareSourceInternal(ExoPlayer, boolean)}. + * #prepareSourceInternal(ExoPlayer, boolean, TransferListener)}. */ protected abstract void releaseSourceInternal(); @@ -130,11 +136,20 @@ public abstract class BaseMediaSource implements MediaSource { @Override public final void prepareSource( ExoPlayer player, boolean isTopLevelSource, SourceInfoRefreshListener listener) { + prepareSource(player, isTopLevelSource, listener, /* mediaTransferListener= */ null); + } + + @Override + public final void prepareSource( + ExoPlayer player, + boolean isTopLevelSource, + SourceInfoRefreshListener listener, + @Nullable TransferListener mediaTransferListener) { Assertions.checkArgument(this.player == null || this.player == player); sourceInfoListeners.add(listener); if (this.player == null) { this.player = player; - prepareSourceInternal(player, isTopLevelSource); + prepareSourceInternal(player, isTopLevelSource, mediaTransferListener); } else if (timeline != null) { listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index f633dd8f15..f494856509 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.lang.annotation.Retention; @@ -211,8 +212,11 @@ public final class ClippingMediaSource extends CompositeMediaSource { } @Override - public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { - super.prepareSourceInternal(player, isTopLevelSource); + public void prepareSourceInternal( + ExoPlayer player, + boolean isTopLevelSource, + @Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(player, isTopLevelSource, mediaTransferListener); prepareChildSource(/* id= */ null, mediaSource); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java index f5c4b3a16d..2ef5186224 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java @@ -20,7 +20,7 @@ import android.support.annotation.CallSuper; import android.support.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -35,8 +35,9 @@ public abstract class CompositeMediaSource extends BaseMediaSource { private final HashMap childSources; - private ExoPlayer player; - private Handler eventHandler; + private @Nullable ExoPlayer player; + private @Nullable Handler eventHandler; + private @Nullable TransferListener mediaTransferListener; /** Create composite media source without child sources. */ protected CompositeMediaSource() { @@ -45,8 +46,12 @@ public abstract class CompositeMediaSource extends BaseMediaSource { @Override @CallSuper - public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + public void prepareSourceInternal( + ExoPlayer player, + boolean isTopLevelSource, + @Nullable TransferListener mediaTransferListener) { this.player = player; + this.mediaTransferListener = mediaTransferListener; eventHandler = new Handler(); } @@ -78,7 +83,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { * @param manifest The manifest of the child source. */ protected abstract void onChildSourceInfoRefreshed( - @Nullable T id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest); + T id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest); /** * Prepares a child source. @@ -93,7 +98,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { * @param id A unique id to identify the child source preparation. Null is allowed as an id. * @param mediaSource The child {@link MediaSource}. */ - protected final void prepareChildSource(@Nullable final T id, MediaSource mediaSource) { + protected final void prepareChildSource(final T id, MediaSource mediaSource) { Assertions.checkArgument(!childSources.containsKey(id)); SourceInfoRefreshListener sourceListener = new SourceInfoRefreshListener() { @@ -105,8 +110,12 @@ public abstract class CompositeMediaSource extends BaseMediaSource { }; MediaSourceEventListener eventListener = new ForwardingEventListener(id); childSources.put(id, new MediaSourceAndListener(mediaSource, sourceListener, eventListener)); - mediaSource.addEventListener(eventHandler, eventListener); - mediaSource.prepareSource(player, /* isTopLevelSource= */ false, sourceListener); + mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener); + mediaSource.prepareSource( + Assertions.checkNotNull(player), + /* isTopLevelSource= */ false, + sourceListener, + mediaTransferListener); } /** @@ -114,8 +123,8 @@ public abstract class CompositeMediaSource extends BaseMediaSource { * * @param id The unique id used to prepare the child source. */ - protected final void releaseChildSource(@Nullable T id) { - MediaSourceAndListener removedChild = childSources.remove(id); + protected final void releaseChildSource(T id) { + MediaSourceAndListener removedChild = Assertions.checkNotNull(childSources.remove(id)); removedChild.mediaSource.releaseSource(removedChild.listener); removedChild.mediaSource.removeEventListener(removedChild.eventListener); } @@ -128,7 +137,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { * @param windowIndex A window index of the child source. * @return The corresponding window index in the composite source. */ - protected int getWindowIndexForChildWindowIndex(@Nullable T id, int windowIndex) { + protected int getWindowIndexForChildWindowIndex(T id, int windowIndex) { return windowIndex; } @@ -143,7 +152,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { * corresponding media period id can be determined. */ protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( - @Nullable T id, MediaPeriodId mediaPeriodId) { + T id, MediaPeriodId mediaPeriodId) { return mediaPeriodId; } @@ -177,10 +186,10 @@ public abstract class CompositeMediaSource extends BaseMediaSource { private final class ForwardingEventListener implements MediaSourceEventListener { - private final @Nullable T id; + private final T id; private EventDispatcher eventDispatcher; - public ForwardingEventListener(@Nullable T id) { + public ForwardingEventListener(T id) { this.eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); this.id = id; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 3e39139918..8987e9cb56 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -16,10 +16,8 @@ package com.google.android.exoplayer2.source; import android.os.Handler; -import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; @@ -28,12 +26,14 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; @@ -49,10 +49,11 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSourcesPublic; @@ -61,42 +62,18 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceHolders; private final MediaSourceHolder query; private final Map mediaSourceByMediaPeriod; - private final List pendingOnCompletionActions; + private final List pendingOnCompletionActions; private final boolean isAtomic; + private final boolean useLazyPreparation; private final Timeline.Window window; - private ExoPlayer player; + private @Nullable ExoPlayer player; + private @Nullable Handler playerApplicationHandler; private boolean listenerNotificationScheduled; private ShuffleOrder shuffleOrder; private int windowCount; private int periodCount; - /** Creates a new concatenating media source. */ - public ConcatenatingMediaSource() { - this(/* isAtomic= */ false, new DefaultShuffleOrder(0)); - } - - /** - * Creates a new concatenating media source. - * - * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated - * as a single item for repeating and shuffling. - */ - public ConcatenatingMediaSource(boolean isAtomic) { - this(isAtomic, new DefaultShuffleOrder(0)); - } - - /** - * Creates a new concatenating media source with a custom shuffle order. - * - * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated - * as a single item for repeating and shuffling. - * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. - */ - public ConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder) { - this(isAtomic, shuffleOrder, new MediaSource[0]); - } - /** * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same * {@link MediaSource} instance to be present more than once in the array. @@ -124,6 +101,25 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(); this.query = new MediaSourceHolder(/* mediaSource= */ null); this.isAtomic = isAtomic; + this.useLazyPreparation = useLazyPreparation; window = new Timeline.Window(); addMediaSources(Arrays.asList(mediaSources)); } @@ -268,6 +265,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSourceNote: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int, * int)} instead. * + *

    Note: If you want to remove a set of contiguous sources, it's preferable to use {@link + * #removeMediaSourceRange(int, int)} instead. + * * @param index The index at which the media source will be removed. This index must be in the * range of 0 <= index < {@link #getSize()}. */ @@ -279,7 +279,10 @@ public class ConcatenatingMediaSource extends CompositeMediaSourceNote: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int, - * int)} instead. + * int, Runnable)} instead. + * + *

    Note: If you want to remove a set of contiguous sources, it's preferable to use {@link + * #removeMediaSourceRange(int, int, Runnable)} instead. * * @param index The index at which the media source will be removed. This index must be in the * range of 0 <= index < {@link #getSize()}. @@ -293,7 +296,61 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(index, null, actionOnCompletion)) + .setPayload(new MessageData(index, null, actionOnCompletion)) + .send(); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); + } + } + + /** + * Removes a range of {@link MediaSource}s from the playlist, by specifying an initial index + * (included) and a final index (excluded). + * + *

    Note: when specified range is empty, no actual media source is removed and no exception is + * thrown. + * + * @param fromIndex The initial range index, pointing to the first media source that will be + * removed. This index must be in the range of 0 <= index <= {@link #getSize()}. + * @param toIndex The final range index, pointing to the first media source that will be left + * untouched. This index must be in the range of 0 <= index <= {@link #getSize()}. + * @throws IndexOutOfBoundsException When the range is malformed, i.e. {@code fromIndex} < 0, + * {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex} + */ + public final synchronized void removeMediaSourceRange(int fromIndex, int toIndex) { + removeMediaSourceRange(fromIndex, toIndex, null); + } + + /** + * Removes a range of {@link MediaSource}s from the playlist, by specifying an initial index + * (included) and a final index (excluded), and executes a custom action on completion. + * + *

    Note: when specified range is empty, no actual media source is removed and no exception is + * thrown. + * + * @param fromIndex The initial range index, pointing to the first media source that will be + * removed. This index must be in the range of 0 <= index <= {@link #getSize()}. + * @param toIndex The final range index, pointing to the first media source that will be left + * untouched. This index must be in the range of 0 <= index <= {@link #getSize()}. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source range has been removed from the playlist. + * @throws IndexOutOfBoundsException When the range is malformed, i.e. {@code fromIndex} < 0, + * {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex} + */ + public final synchronized void removeMediaSourceRange( + int fromIndex, int toIndex, @Nullable Runnable actionOnCompletion) { + Util.removeRange(mediaSourcesPublic, fromIndex, toIndex); + if (fromIndex == toIndex) { + if (actionOnCompletion != null) { + actionOnCompletion.run(); + } + return; + } + if (player != null) { + player + .createMessage(this) + .setType(MSG_REMOVE_RANGE) + .setPayload(new MessageData<>(fromIndex, toIndex, actionOnCompletion)) .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); @@ -354,11 +411,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource addMessage = (MessageData) message; @@ -483,6 +548,18 @@ public class ConcatenatingMediaSource extends CompositeMediaSource removeRangeMessage = (MessageData) message; + int fromIndex = removeRangeMessage.index; + int toIndex = removeRangeMessage.customData; + for (int index = toIndex - 1; index >= fromIndex; index--) { + shuffleOrder = shuffleOrder.cloneAndRemove(index); + } + for (int index = toIndex - 1; index >= fromIndex; index--) { + removeMediaSourceInternal(index); + } + scheduleListenerNotification(removeRangeMessage.actionOnCompletion); + break; case MSG_MOVE: MessageData moveMessage = (MessageData) message; shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index); @@ -492,15 +569,16 @@ public class ConcatenatingMediaSource extends CompositeMediaSource actionsOnCompletion = ((List) message); + List actionsOnCompletion = ((List) message); + Handler handler = Assertions.checkNotNull(playerApplicationHandler); for (int i = 0; i < actionsOnCompletion.size(); i++) { - actionsOnCompletion.get(i).dispatchEvent(); + handler.post(actionsOnCompletion.get(i)); } break; default: @@ -508,9 +586,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource actionsOnCompletion = + List actionsOnCompletion = pendingOnCompletionActions.isEmpty() - ? Collections.emptyList() + ? Collections.emptyList() : new ArrayList<>(pendingOnCompletionActions); pendingOnCompletionActions.clear(); refreshSourceInfo( @@ -530,7 +608,11 @@ public class ConcatenatingMediaSource extends CompositeMediaSource { public final MediaSource mediaSource; - public final int uid; + public final Object uid; public DeferredTimeline timeline; public int childIndex; public int firstWindowIndexInChild; public int firstPeriodIndexInChild; + public boolean hasStartedPreparing; public boolean isPrepared; public boolean isRemoved; public List activeMediaPeriods; public MediaSourceHolder(MediaSource mediaSource) { this.mediaSource = mediaSource; - this.uid = System.identityHashCode(this); this.timeline = new DeferredTimeline(); this.activeMediaPeriods = new ArrayList<>(); + this.uid = new Object(); } public void reset(int childIndex, int firstWindowIndexInChild, int firstPeriodIndexInChild) { this.childIndex = childIndex; this.firstWindowIndexInChild = firstWindowIndexInChild; this.firstPeriodIndexInChild = firstPeriodIndexInChild; + this.hasStartedPreparing = false; this.isPrepared = false; this.isRemoved = false; this.activeMediaPeriods.clear(); @@ -688,34 +778,16 @@ public class ConcatenatingMediaSource extends CompositeMediaSource { public final int index; public final T customData; - public final @Nullable EventDispatcher actionOnCompletion; + public final @Nullable Runnable actionOnCompletion; public MessageData(int index, T customData, @Nullable Runnable actionOnCompletion) { this.index = index; - this.actionOnCompletion = - actionOnCompletion != null ? new EventDispatcher(actionOnCompletion) : null; + this.actionOnCompletion = actionOnCompletion; this.customData = customData; } } @@ -728,8 +800,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource childIndexByUid; public ConcatenatedTimeline( Collection mediaSourceHolders, @@ -744,8 +816,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(); int index = 0; for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { timelines[index] = mediaSourceHolder.timeline; @@ -768,11 +840,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource 0 - ? timeline.getPeriod(0, period, true).uid + replacedId == DUMMY_ID && timeline.getPeriodCount() > 0 + ? timeline.getUidOfPeriod(0) : replacedId); } @@ -853,6 +920,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource 0 ? C.TIME_UNSET : 0, + /* defaultPositionUs= */ 0, /* durationUs= */ C.TIME_UNSET, /* firstPeriodIndex= */ 0, /* lastPeriodIndex= */ 0, @@ -889,8 +961,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource { + if (!released) { + Assertions.checkNotNull(callback).onContinueLoadingRequested(ExtractorMediaPeriod.this); + } + }; handler = new Handler(); sampleQueueTrackIds = new int[0]; sampleQueues = new SampleQueue[0]; pendingResetPositionUs = C.TIME_UNSET; length = C.LENGTH_UNSET; durationUs = C.TIME_UNSET; - // Assume on-demand for MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, until prepared. - actualMinLoadableRetryCount = - minLoadableRetryCount == ExtractorMediaSource.MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA - ? ExtractorMediaSource.DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND - : minLoadableRetryCount; + dataType = C.DATA_TYPE_MEDIA; eventDispatcher.mediaPeriodCreated(); } @@ -188,8 +181,9 @@ import java.util.Arrays; sampleQueue.discardToEnd(); } } - loader.release(this); + loader.release(/* callback= */ this); handler.removeCallbacksAndMessages(null); + callback = null; released = true; eventDispatcher.mediaPeriodReleased(); } @@ -216,13 +210,19 @@ import java.util.Arrays; @Override public TrackGroupArray getTrackGroups() { - return tracks; + return getPreparedState().tracks; } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - Assertions.checkState(prepared); + public long selectTracks( + TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { + PreparedState preparedState = getPreparedState(); + TrackGroupArray tracks = preparedState.tracks; + boolean[] trackEnabledStates = preparedState.trackEnabledStates; int oldEnabledTrackCount = enabledTrackCount; // Deselect old tracks. for (int i = 0; i < selections.length; i++) { @@ -291,6 +291,7 @@ import java.util.Arrays; @Override public void discardBuffer(long positionUs, boolean toKeyframe) { + boolean[] trackEnabledStates = getPreparedState().trackEnabledStates; int trackCount = sampleQueues.length; for (int i = 0; i < trackCount; i++) { sampleQueues[i].discardTo(positionUs, toKeyframe, trackEnabledStates[i]); @@ -336,6 +337,7 @@ import java.util.Arrays; @Override public long getBufferedPositionUs() { + boolean[] trackIsAudioVideoFlags = getPreparedState().trackIsAudioVideoFlags; if (loadingFinished) { return C.TIME_END_OF_SOURCE; } else if (isPendingReset()) { @@ -361,12 +363,17 @@ import java.util.Arrays; @Override public long seekToUs(long positionUs) { + PreparedState preparedState = getPreparedState(); + SeekMap seekMap = preparedState.seekMap; + boolean[] trackIsAudioVideoFlags = preparedState.trackIsAudioVideoFlags; // Treat all seeks into non-seekable media as being to t=0. positionUs = seekMap.isSeekable() ? positionUs : 0; lastSeekPositionUs = positionUs; notifyDiscontinuity = false; - // If we're not pending a reset, see if we can seek within the buffer. - if (!isPendingReset() && seekInsideBufferUs(positionUs)) { + // If we're not playing a live stream or pending a reset, see if we can seek within the buffer. + if (dataType != C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE + && !isPendingReset() + && seekInsideBufferUs(trackIsAudioVideoFlags, positionUs)) { return positionUs; } // We were unable to seek within the buffer, so need to reset. @@ -385,6 +392,7 @@ import java.util.Arrays; @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + SeekMap seekMap = getPreparedState().seekMap; if (!seekMap.isSeekable()) { // Treat all seeks into non-seekable media as being to t=0. return 0; @@ -401,7 +409,7 @@ import java.util.Arrays; } /* package */ void maybeThrowError() throws IOException { - loader.maybeThrowError(actualMinLoadableRetryCount); + loader.maybeThrowError(loadErrorHandlingPolicy.getMinimumLoadableRetryCount(dataType)); } /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer, @@ -443,6 +451,9 @@ import java.util.Arrays; } private void maybeNotifyTrackFormat(int track) { + PreparedState preparedState = getPreparedState(); + boolean[] trackFormatNotificationSent = preparedState.trackFormatNotificationSent; + TrackGroupArray tracks = preparedState.tracks; if (!trackFormatNotificationSent[track]) { Format trackFormat = tracks.get(track).getFormat(0); eventDispatcher.downstreamFormatChanged( @@ -456,6 +467,7 @@ import java.util.Arrays; } private void maybeStartDeferredRetry(int track) { + boolean[] trackIsAudioVideoFlags = getPreparedState().trackIsAudioVideoFlags; if (!pendingDeferredRetry || !trackIsAudioVideoFlags[track] || sampleQueues[track].hasNextSample()) { @@ -469,7 +481,7 @@ import java.util.Arrays; for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.reset(); } - callback.onContinueLoadingRequested(this); + Assertions.checkNotNull(callback).onContinueLoadingRequested(this); } private boolean suppressRead() { @@ -482,6 +494,7 @@ import java.util.Arrays; public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { if (durationUs == C.TIME_UNSET) { + SeekMap seekMap = Assertions.checkNotNull(this.seekMap); long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0 : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US; @@ -489,6 +502,7 @@ import java.util.Arrays; } eventDispatcher.loadCompleted( loadable.dataSpec, + loadable.dataSource.getLastOpenedUri(), C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, /* trackFormat= */ null, @@ -498,10 +512,10 @@ import java.util.Arrays; durationUs, elapsedRealtimeMs, loadDurationMs, - loadable.bytesLoaded); + loadable.dataSource.getBytesRead()); copyLengthFromLoader(loadable); loadingFinished = true; - callback.onContinueLoadingRequested(this); + Assertions.checkNotNull(callback).onContinueLoadingRequested(this); } @Override @@ -509,6 +523,7 @@ import java.util.Arrays; long loadDurationMs, boolean released) { eventDispatcher.loadCanceled( loadable.dataSpec, + loadable.dataSource.getLastOpenedUri(), C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, /* trackFormat= */ null, @@ -518,24 +533,43 @@ import java.util.Arrays; durationUs, elapsedRealtimeMs, loadDurationMs, - loadable.bytesLoaded); + loadable.dataSource.getBytesRead()); if (!released) { copyLengthFromLoader(loadable); for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.reset(); } if (enabledTrackCount > 0) { - callback.onContinueLoadingRequested(this); + Assertions.checkNotNull(callback).onContinueLoadingRequested(this); } } } @Override - public @Loader.RetryAction int onLoadError( - ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { - boolean isErrorFatal = isLoadableExceptionFatal(error); + public LoadErrorAction onLoadError( + ExtractingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error, + int errorCount) { + copyLengthFromLoader(loadable); + LoadErrorAction loadErrorAction; + long retryDelayMs = + loadErrorHandlingPolicy.getRetryDelayMsFor(dataType, durationUs, error, errorCount); + if (retryDelayMs == C.TIME_UNSET) { + loadErrorAction = Loader.DONT_RETRY_FATAL; + } else /* the load should be retried */ { + int extractedSamplesCount = getExtractedSamplesCount(); + boolean madeProgress = extractedSamplesCount > extractedSamplesCountAtStartOfLoad; + loadErrorAction = + configureRetry(loadable, extractedSamplesCount) + ? Loader.createRetryAction(/* resetErrorCount= */ madeProgress, retryDelayMs) + : Loader.DONT_RETRY; + } + eventDispatcher.loadError( loadable.dataSpec, + loadable.dataSource.getLastOpenedUri(), C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, /* trackFormat= */ null, @@ -545,18 +579,10 @@ import java.util.Arrays; durationUs, elapsedRealtimeMs, loadDurationMs, - loadable.bytesLoaded, + loadable.dataSource.getBytesRead(), error, - /* wasCanceled= */ isErrorFatal); - copyLengthFromLoader(loadable); - if (isErrorFatal) { - return Loader.DONT_RETRY_FATAL; - } - int extractedSamplesCount = getExtractedSamplesCount(); - boolean madeProgress = extractedSamplesCount > extractedSamplesCountAtStartOfLoad; - return configureRetry(loadable, extractedSamplesCount) - ? (madeProgress ? Loader.RETRY_RESET_ERROR_COUNT : Loader.RETRY) - : Loader.DONT_RETRY; + !loadErrorAction.isRetry()); + return loadErrorAction; } // ExtractorOutput implementation. Called by the loading thread. @@ -573,8 +599,9 @@ import java.util.Arrays; trackOutput.setUpstreamFormatChangeListener(this); sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1); sampleQueueTrackIds[trackCount] = id; - sampleQueues = Arrays.copyOf(sampleQueues, trackCount + 1); + @NullableType SampleQueue[] sampleQueues = Arrays.copyOf(this.sampleQueues, trackCount + 1); sampleQueues[trackCount] = trackOutput; + this.sampleQueues = Util.castNonNullTypeArray(sampleQueues); return trackOutput; } @@ -600,7 +627,8 @@ import java.util.Arrays; // Internal methods. private void maybeFinishPrepare() { - if (released || prepared || seekMap == null || !sampleQueuesBuilt) { + SeekMap seekMap = this.seekMap; + if (released || prepared || !sampleQueuesBuilt || seekMap == null) { return; } for (SampleQueue sampleQueue : sampleQueues) { @@ -611,9 +639,7 @@ import java.util.Arrays; loadCondition.close(); int trackCount = sampleQueues.length; TrackGroup[] trackArray = new TrackGroup[trackCount]; - trackIsAudioVideoFlags = new boolean[trackCount]; - trackEnabledStates = new boolean[trackCount]; - trackFormatNotificationSent = new boolean[trackCount]; + boolean[] trackIsAudioVideoFlags = new boolean[trackCount]; durationUs = seekMap.getDurationUs(); for (int i = 0; i < trackCount; i++) { Format trackFormat = sampleQueues[i].getUpstreamFormat(); @@ -623,14 +649,24 @@ import java.util.Arrays; trackIsAudioVideoFlags[i] = isAudioVideo; haveAudioVideoTracks |= isAudioVideo; } - tracks = new TrackGroupArray(trackArray); - if (minLoadableRetryCount == ExtractorMediaSource.MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA - && length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET) { - actualMinLoadableRetryCount = ExtractorMediaSource.DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE; - } + dataType = + length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET + ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE + : C.DATA_TYPE_MEDIA; + preparedState = + new PreparedState( + new TrackGroupArray(trackArray), + /* trackEnabledStates= */ new boolean[trackCount], + trackIsAudioVideoFlags, + /* trackFormatNotificationSent= */ new boolean[trackCount], + seekMap); prepared = true; listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable()); - callback.onPrepared(this); + Assertions.checkNotNull(callback).onPrepared(this); + } + + private PreparedState getPreparedState() { + return Assertions.checkNotNull(preparedState); } private void copyLengthFromLoader(ExtractingLoadable loadable) { @@ -640,9 +676,11 @@ import java.util.Arrays; } private void startLoading() { - ExtractingLoadable loadable = new ExtractingLoadable(uri, dataSource, extractorHolder, - loadCondition); + ExtractingLoadable loadable = + new ExtractingLoadable( + uri, dataSource, extractorHolder, /* extractorOutput= */ this, loadCondition); if (prepared) { + SeekMap seekMap = getPreparedState().seekMap; Assertions.checkState(isPendingReset()); if (durationUs != C.TIME_UNSET && pendingResetPositionUs >= durationUs) { loadingFinished = true; @@ -654,9 +692,12 @@ import java.util.Arrays; pendingResetPositionUs = C.TIME_UNSET; } extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); - long elapsedRealtimeMs = loader.startLoading(loadable, this, actualMinLoadableRetryCount); + long elapsedRealtimeMs = + loader.startLoading( + loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(dataType)); eventDispatcher.loadStarted( loadable.dataSpec, + loadable.dataSpec.uri, C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, /* trackFormat= */ null, @@ -714,10 +755,11 @@ import java.util.Arrays; /** * Attempts to seek to the specified position within the sample queues. * + * @param trackIsAudioVideoFlags Whether each track is audio/video. * @param positionUs The seek position in microseconds. * @return Whether the in-buffer seek was successful. */ - private boolean seekInsideBufferUs(long positionUs) { + private boolean seekInsideBufferUs(boolean[] trackIsAudioVideoFlags, long positionUs) { int trackCount = sampleQueues.length; for (int i = 0; i < trackCount; i++) { SampleQueue sampleQueue = sampleQueues[i]; @@ -756,10 +798,6 @@ import java.util.Arrays; return pendingResetPositionUs != C.TIME_UNSET; } - private static boolean isLoadableExceptionFatal(IOException e) { - return e instanceof UnrecognizedInputFormatException; - } - private final class SampleStreamImpl implements SampleStream { private final int track; @@ -791,14 +829,13 @@ import java.util.Arrays; } - /** - * Loads the media stream and extracts sample data from it. - */ + /** Loads the media stream and extracts sample data from it. */ /* package */ final class ExtractingLoadable implements Loadable { private final Uri uri; - private final DataSource dataSource; + private final StatsDataSource dataSource; private final ExtractorHolder extractorHolder; + private final ExtractorOutput extractorOutput; private final ConditionVariable loadCondition; private final PositionHolder positionHolder; @@ -808,35 +845,31 @@ import java.util.Arrays; private long seekTimeUs; private DataSpec dataSpec; private long length; - private long bytesLoaded; - public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder, + public ExtractingLoadable( + Uri uri, + DataSource dataSource, + ExtractorHolder extractorHolder, + ExtractorOutput extractorOutput, ConditionVariable loadCondition) { - this.uri = Assertions.checkNotNull(uri); - this.dataSource = Assertions.checkNotNull(dataSource); - this.extractorHolder = Assertions.checkNotNull(extractorHolder); + this.uri = uri; + this.dataSource = new StatsDataSource(dataSource); + this.extractorHolder = extractorHolder; + this.extractorOutput = extractorOutput; this.loadCondition = loadCondition; this.positionHolder = new PositionHolder(); this.pendingExtractorSeek = true; this.length = C.LENGTH_UNSET; + dataSpec = new DataSpec(uri, positionHolder.position, C.LENGTH_UNSET, customCacheKey); } - public void setLoadPosition(long position, long timeUs) { - positionHolder.position = position; - seekTimeUs = timeUs; - pendingExtractorSeek = true; - } + // Loadable implementation. @Override public void cancelLoad() { loadCanceled = true; } - @Override - public boolean isLoadCanceled() { - return loadCanceled; - } - @Override public void load() throws IOException, InterruptedException { int result = Extractor.RESULT_CONTINUE; @@ -849,8 +882,9 @@ import java.util.Arrays; if (length != C.LENGTH_UNSET) { length += position; } + Uri uri = Assertions.checkNotNull(dataSource.getUri()); input = new DefaultExtractorInput(dataSource, position, length); - Extractor extractor = extractorHolder.selectExtractor(input, dataSource.getUri()); + Extractor extractor = extractorHolder.selectExtractor(input, extractorOutput, uri); if (pendingExtractorSeek) { extractor.seek(position, seekTimeUs); pendingExtractorSeek = false; @@ -869,33 +903,35 @@ import java.util.Arrays; result = Extractor.RESULT_CONTINUE; } else if (input != null) { positionHolder.position = input.getPosition(); - bytesLoaded = positionHolder.position - dataSpec.absoluteStreamPosition; } Util.closeQuietly(dataSource); } } } + // Internal methods. + + private void setLoadPosition(long position, long timeUs) { + positionHolder.position = position; + seekTimeUs = timeUs; + pendingExtractorSeek = true; + } } - /** - * Stores a list of extractors and a selected extractor when the format has been detected. - */ + /** Stores a list of extractors and a selected extractor when the format has been detected. */ private static final class ExtractorHolder { private final Extractor[] extractors; - private final ExtractorOutput extractorOutput; - private Extractor extractor; + + private @Nullable Extractor extractor; /** * Creates a holder that will select an extractor and initialize it using the specified output. * * @param extractors One or more extractors to choose from. - * @param extractorOutput The output that will be used to initialize the selected extractor. */ - public ExtractorHolder(Extractor[] extractors, ExtractorOutput extractorOutput) { + public ExtractorHolder(Extractor[] extractors) { this.extractors = extractors; - this.extractorOutput = extractorOutput; } /** @@ -903,13 +939,15 @@ import java.util.Arrays; * later calls. * * @param input The {@link ExtractorInput} from which data should be read. + * @param output The {@link ExtractorOutput} that will be used to initialize the selected + * extractor. * @param uri The {@link Uri} of the data. * @return An initialized extractor for reading {@code input}. * @throws UnrecognizedInputFormatException Thrown if the input format could not be detected. * @throws IOException Thrown if the input could not be read. * @throws InterruptedException Thrown if the thread was interrupted. */ - public Extractor selectExtractor(ExtractorInput input, Uri uri) + public Extractor selectExtractor(ExtractorInput input, ExtractorOutput output, Uri uri) throws IOException, InterruptedException { if (extractor != null) { return extractor; @@ -930,7 +968,7 @@ import java.util.Arrays; throw new UnrecognizedInputFormatException("None of the available extractors (" + Util.getCommaDelimitedSimpleClassNames(extractors) + ") could read the stream.", uri); } - extractor.init(extractorOutput); + extractor.init(output); return extractor; } @@ -940,7 +978,27 @@ import java.util.Arrays; extractor = null; } } - } + /** Stores state that is initialized when preparation completes. */ + private static final class PreparedState { + public final TrackGroupArray tracks; + public final boolean[] trackEnabledStates; + public final boolean[] trackIsAudioVideoFlags; + public final boolean[] trackFormatNotificationSent; + public final SeekMap seekMap; + + public PreparedState( + TrackGroupArray tracks, + boolean[] trackEnabledStates, + boolean[] trackIsAudioVideoFlags, + boolean[] trackFormatNotificationSent, + SeekMap seekMap) { + this.tracks = tracks; + this.trackEnabledStates = trackEnabledStates; + this.trackIsAudioVideoFlags = trackIsAudioVideoFlags; + this.trackFormatNotificationSent = trackFormatNotificationSent; + this.seekMap = seekMap; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index c4a0487bd9..b8db9adde6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -27,6 +27,9 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -43,6 +46,7 @@ import java.io.IOException; */ public final class ExtractorMediaSource extends BaseMediaSource implements ExtractorMediaPeriod.Listener { + /** * Listener of {@link ExtractorMediaSource} events. * @@ -67,40 +71,6 @@ public final class ExtractorMediaSource extends BaseMediaSource } - /** - * The default minimum number of times to retry loading prior to failing for on-demand streams. - */ - public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND = 3; - - /** - * The default minimum number of times to retry loading prior to failing for live streams. - */ - public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE = 6; - - /** - * Value for {@code minLoadableRetryCount} that causes the loader to retry - * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE} times for live streams and - * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND} for on-demand streams. - */ - public static final int MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA = -1; - - /** - * The default number of bytes that should be loaded between each each invocation of - * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. - */ - public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024; - - private final Uri uri; - private final DataSource.Factory dataSourceFactory; - private final ExtractorsFactory extractorsFactory; - private final int minLoadableRetryCount; - private final String customCacheKey; - private final int continueLoadingCheckIntervalBytes; - private final @Nullable Object tag; - - private long timelineDurationUs; - private boolean timelineIsSeekable; - /** Factory for {@link ExtractorMediaSource}s. */ public static final class Factory implements AdsMediaSource.MediaSourceFactory { @@ -109,7 +79,7 @@ public final class ExtractorMediaSource extends BaseMediaSource private @Nullable ExtractorsFactory extractorsFactory; private @Nullable String customCacheKey; private @Nullable Object tag; - private int minLoadableRetryCount; + private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private int continueLoadingCheckIntervalBytes; private boolean isCreateCalled; @@ -120,7 +90,7 @@ public final class ExtractorMediaSource extends BaseMediaSource */ public Factory(DataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; - minLoadableRetryCount = MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA; + loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; } @@ -171,16 +141,36 @@ public final class ExtractorMediaSource extends BaseMediaSource } /** - * Sets the minimum number of times to retry if a loading error occurs. The default value is - * {@link #MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA}. + * Sets the minimum number of times to retry if a loading error occurs. See {@link + * #setLoadErrorHandlingPolicy} for the default value. + * + *

    Calling this method is equivalent to calling {@link #setLoadErrorHandlingPolicy} with + * {@link DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy(int) + * DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)} * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead. */ + @Deprecated public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)); + } + + /** + * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link + * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. + * + *

    Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}. + * + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { Assertions.checkState(!isCreateCalled); - this.minLoadableRetryCount = minLoadableRetryCount; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; return this; } @@ -217,7 +207,7 @@ public final class ExtractorMediaSource extends BaseMediaSource uri, dataSourceFactory, extractorsFactory, - minLoadableRetryCount, + loadErrorHandlingPolicy, customCacheKey, continueLoadingCheckIntervalBytes, tag); @@ -243,6 +233,24 @@ public final class ExtractorMediaSource extends BaseMediaSource } } + /** + * The default number of bytes that should be loaded between each each invocation of {@link + * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + */ + public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024; + + private final Uri uri; + private final DataSource.Factory dataSourceFactory; + private final ExtractorsFactory extractorsFactory; + private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy; + private final String customCacheKey; + private final int continueLoadingCheckIntervalBytes; + private final @Nullable Object tag; + + private long timelineDurationUs; + private boolean timelineIsSeekable; + private @Nullable TransferListener transferListener; + /** * @param uri The {@link Uri} of the media stream. * @param dataSourceFactory A factory for {@link DataSource}s to read the media. @@ -283,8 +291,14 @@ public final class ExtractorMediaSource extends BaseMediaSource Handler eventHandler, EventListener eventListener, String customCacheKey) { - this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler, - eventListener, customCacheKey, DEFAULT_LOADING_CHECK_INTERVAL_BYTES); + this( + uri, + dataSourceFactory, + extractorsFactory, + eventHandler, + eventListener, + customCacheKey, + DEFAULT_LOADING_CHECK_INTERVAL_BYTES); } /** @@ -293,7 +307,6 @@ public final class ExtractorMediaSource extends BaseMediaSource * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the * possible formats are known, pass a factory that instantiates extractors for those formats. * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors. - * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache @@ -307,7 +320,6 @@ public final class ExtractorMediaSource extends BaseMediaSource Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, - int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, String customCacheKey, @@ -316,7 +328,7 @@ public final class ExtractorMediaSource extends BaseMediaSource uri, dataSourceFactory, extractorsFactory, - minLoadableRetryCount, + new DefaultLoadErrorHandlingPolicy(), customCacheKey, continueLoadingCheckIntervalBytes, /* tag= */ null); @@ -329,14 +341,14 @@ public final class ExtractorMediaSource extends BaseMediaSource Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, - int minLoadableRetryCount, + LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy, @Nullable String customCacheKey, int continueLoadingCheckIntervalBytes, @Nullable Object tag) { this.uri = uri; this.dataSourceFactory = dataSourceFactory; this.extractorsFactory = extractorsFactory; - this.minLoadableRetryCount = minLoadableRetryCount; + this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy; this.customCacheKey = customCacheKey; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; this.timelineDurationUs = C.TIME_UNSET; @@ -344,7 +356,11 @@ public final class ExtractorMediaSource extends BaseMediaSource } @Override - public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + public void prepareSourceInternal( + ExoPlayer player, + boolean isTopLevelSource, + @Nullable TransferListener mediaTransferListener) { + transferListener = mediaTransferListener; notifySourceInfoRefreshed(timelineDurationUs, /* isSeekable= */ false); } @@ -356,11 +372,15 @@ public final class ExtractorMediaSource extends BaseMediaSource @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); + DataSource dataSource = dataSourceFactory.createDataSource(); + if (transferListener != null) { + dataSource.addTransferListener(transferListener); + } return new ExtractorMediaPeriod( uri, - dataSourceFactory.createDataSource(), + dataSource, extractorsFactory.createExtractors(), - minLoadableRetryCount, + loadableLoadErrorHandlingPolicy, createEventDispatcher(id), this, allocator, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java index c7ab7615d8..45997aced4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java @@ -77,4 +77,8 @@ public abstract class ForwardingTimeline extends Timeline { return timeline.getIndexOfPeriod(uid); } + @Override + public Object getUidOfPeriod(int periodIndex) { + return timeline.getUidOfPeriod(periodIndex); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 774074b016..e83138cd33 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -22,7 +22,10 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; +import java.util.HashMap; +import java.util.Map; /** * Loops a {@link MediaSource} a specified number of times. @@ -34,6 +37,8 @@ public final class LoopingMediaSource extends CompositeMediaSource { private final MediaSource childSource; private final int loopCount; + private final Map childMediaPeriodIdToMediaPeriodId; + private final Map mediaPeriodToChildMediaPeriodId; private int childPeriodCount; @@ -57,25 +62,38 @@ public final class LoopingMediaSource extends CompositeMediaSource { Assertions.checkArgument(loopCount > 0); this.childSource = childSource; this.loopCount = loopCount; + childMediaPeriodIdToMediaPeriodId = new HashMap<>(); + mediaPeriodToChildMediaPeriodId = new HashMap<>(); } @Override - public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { - super.prepareSourceInternal(player, isTopLevelSource); + public void prepareSourceInternal( + ExoPlayer player, + boolean isTopLevelSource, + @Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(player, isTopLevelSource, mediaTransferListener); prepareChildSource(/* id= */ null, childSource); } @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - return loopCount != Integer.MAX_VALUE - ? childSource.createPeriod(id.copyWithPeriodIndex(id.periodIndex % childPeriodCount), - allocator) - : childSource.createPeriod(id, allocator); + if (loopCount == Integer.MAX_VALUE) { + return childSource.createPeriod(id, allocator); + } + MediaPeriodId childMediaPeriodId = id.copyWithPeriodIndex(id.periodIndex % childPeriodCount); + childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id); + MediaPeriod mediaPeriod = childSource.createPeriod(childMediaPeriodId, allocator); + mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId); + return mediaPeriod; } @Override public void releasePeriod(MediaPeriod mediaPeriod) { childSource.releasePeriod(mediaPeriod); + MediaPeriodId childMediaPeriodId = mediaPeriodToChildMediaPeriodId.remove(mediaPeriod); + if (childMediaPeriodId != null) { + childMediaPeriodIdToMediaPeriodId.remove(childMediaPeriodId); + } } @Override @@ -95,6 +113,14 @@ public final class LoopingMediaSource extends CompositeMediaSource { refreshSourceInfo(loopingTimeline, manifest); } + @Override + protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + Void id, MediaPeriodId mediaPeriodId) { + return loopCount != Integer.MAX_VALUE + ? childMediaPeriodIdToMediaPeriodId.get(mediaPeriodId) + : mediaPeriodId; + } + private static final class LoopingTimeline extends AbstractConcatenatedTimeline { private final Timeline childTimeline; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 1a243a8bf0..fb4c64ae6e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; import java.io.IOException; /** @@ -32,7 +33,7 @@ import java.io.IOException; * provide a new timeline whenever the structure of the media changes. The MediaSource * provides these timelines by calling {@link SourceInfoRefreshListener#onSourceInfoRefreshed} * on the {@link SourceInfoRefreshListener}s passed to {@link #prepareSource(ExoPlayer, - * boolean, SourceInfoRefreshListener)}. + * boolean, SourceInfoRefreshListener, TransferListener)}. *

  • To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator)}, and provide a way for * the player to load and read the media. @@ -89,6 +90,15 @@ public interface MediaSource { */ public final long windowSequenceNumber; + /** + * The end position of the media to play within the media period, in microseconds, or {@link + * C#TIME_END_OF_SOURCE} if the end position is the end of the media period. + * + *

    Note that this only applies if the media period is for content (i.e., not for an ad) and + * is clipped to the position of the next ad group. + */ + public final long endPositionUs; + /** * Creates a media period identifier for a dummy period which is not part of a buffered sequence * of windows. @@ -107,7 +117,20 @@ public interface MediaSource { * windows this media period is part of. */ public MediaPeriodId(int periodIndex, long windowSequenceNumber) { - this(periodIndex, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber); + this(periodIndex, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber, C.TIME_END_OF_SOURCE); + } + + /** + * Creates a media period identifier for the specified clipped period in the timeline. + * + * @param periodIndex The timeline period index. + * @param windowSequenceNumber The sequence number of the window in the buffered sequence of + * windows this media period is part of. + * @param endPositionUs The end position of the media period within the timeline period, in + * microseconds. + */ + public MediaPeriodId(int periodIndex, long windowSequenceNumber, long endPositionUs) { + this(periodIndex, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber, endPositionUs); } /** @@ -122,10 +145,20 @@ public interface MediaSource { */ public MediaPeriodId( int periodIndex, int adGroupIndex, int adIndexInAdGroup, long windowSequenceNumber) { + this(periodIndex, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, C.TIME_END_OF_SOURCE); + } + + private MediaPeriodId( + int periodIndex, + int adGroupIndex, + int adIndexInAdGroup, + long windowSequenceNumber, + long endPositionUs) { this.periodIndex = periodIndex; this.adGroupIndex = adGroupIndex; this.adIndexInAdGroup = adIndexInAdGroup; this.windowSequenceNumber = windowSequenceNumber; + this.endPositionUs = endPositionUs; } /** @@ -134,7 +167,8 @@ public interface MediaSource { public MediaPeriodId copyWithPeriodIndex(int newPeriodIndex) { return periodIndex == newPeriodIndex ? this - : new MediaPeriodId(newPeriodIndex, adGroupIndex, adIndexInAdGroup, windowSequenceNumber); + : new MediaPeriodId( + newPeriodIndex, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, endPositionUs); } /** @@ -157,7 +191,8 @@ public interface MediaSource { return periodIndex == periodId.periodIndex && adGroupIndex == periodId.adGroupIndex && adIndexInAdGroup == periodId.adIndexInAdGroup - && windowSequenceNumber == periodId.windowSequenceNumber; + && windowSequenceNumber == periodId.windowSequenceNumber + && endPositionUs == periodId.endPositionUs; } @Override @@ -167,6 +202,7 @@ public interface MediaSource { result = 31 * result + adGroupIndex; result = 31 * result + adIndexInAdGroup; result = 31 * result + (int) windowSequenceNumber; + result = 31 * result + (int) endPositionUs; return result; } @@ -189,6 +225,11 @@ public interface MediaSource { */ void removeEventListener(MediaSourceEventListener eventListener); + /** @deprecated Will be removed in the next release. */ + @Deprecated + void prepareSource( + ExoPlayer player, boolean isTopLevelSource, SourceInfoRefreshListener listener); + /** * Starts source preparation if not yet started, and adds a listener for timeline and/or manifest * updates. @@ -206,9 +247,16 @@ public interface MediaSource { * boolean)}. If {@code false}, this source is being prepared by another source (e.g. {@link * ConcatenatingMediaSource}) for composition. * @param listener The listener to be added. + * @param mediaTransferListener The transfer listener which should be informed of any media data + * transfers. May be null if no listener is available. Note that this listener should be only + * informed of transfers related to the media loads and not of auxiliary loads for manifests + * and other data. */ void prepareSource( - ExoPlayer player, boolean isTopLevelSource, SourceInfoRefreshListener listener); + ExoPlayer player, + boolean isTopLevelSource, + SourceInfoRefreshListener listener, + @Nullable TransferListener mediaTransferListener); /** * Throws any pending error encountered while loading or refreshing source information. @@ -219,10 +267,9 @@ public interface MediaSource { /** * Returns a new {@link MediaPeriod} identified by {@code periodId}. This method may be called - * multiple times with the same period identifier without an intervening call to - * {@link #releasePeriod(MediaPeriod)}. - *

    - * Should not be called directly from application code. + * multiple times without an intervening call to {@link #releasePeriod(MediaPeriod)}. + * + *

    Should not be called directly from application code. * * @param id The identifier of the period. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java index 9d1ba10866..844534a43d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; @@ -35,8 +36,14 @@ public interface MediaSourceEventListener { /** Media source load event information. */ final class LoadEventInfo { - /** Defines the data being loaded. */ + /** Defines the requested data. */ public final DataSpec dataSpec; + /** + * The {@link Uri} from which data is being read. The uri will be identical to the one in {@link + * #dataSpec}.uri unless redirection has occurred. If redirection has occurred, this is the uri + * after redirection. + */ + public final Uri uri; /** The value of {@link SystemClock#elapsedRealtime} at the time of the load event. */ public final long elapsedRealtimeMs; /** The duration of the load up to the event time. */ @@ -47,15 +54,20 @@ public interface MediaSourceEventListener { /** * Creates load event info. * - * @param dataSpec Defines the data being loaded. + * @param dataSpec Defines the requested data. + * @param uri The {@link Uri} from which data is being read. The uri must be identical to the + * one in {@code dataSpec.uri} unless redirection has occurred. If redirection has occurred, + * this is the uri after redirection. * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} at the time of the * load event. * @param loadDurationMs The duration of the load up to the event time. - * @param bytesLoaded The number of bytes that were loaded up to the event time. + * @param bytesLoaded The number of bytes that were loaded up to the event time. For compressed + * network responses, this is the decompressed size. */ public LoadEventInfo( - DataSpec dataSpec, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) { + DataSpec dataSpec, Uri uri, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) { this.dataSpec = dataSpec; + this.uri = uri; this.elapsedRealtimeMs = elapsedRealtimeMs; this.loadDurationMs = loadDurationMs; this.bytesLoaded = bytesLoaded; @@ -155,7 +167,8 @@ public interface MediaSourceEventListener { * @param windowIndex The window index in the timeline of the media source this load belongs to. * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not * belong to a specific media period. - * @param loadEventInfo The {@link LoadEventInfo} defining the load event. + * @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The value of {@link + * LoadEventInfo#uri} won't reflect potential redirection yet. * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. */ void onLoadStarted( @@ -170,7 +183,10 @@ public interface MediaSourceEventListener { * @param windowIndex The window index in the timeline of the media source this load belongs to. * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not * belong to a specific media period. - * @param loadEventInfo The {@link LoadEventInfo} defining the load event. + * @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The values of {@link + * LoadEventInfo#elapsedRealtimeMs} and {@link LoadEventInfo#bytesLoaded} are relative to the + * corresponding {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)} + * event. * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. */ void onLoadCompleted( @@ -185,7 +201,10 @@ public interface MediaSourceEventListener { * @param windowIndex The window index in the timeline of the media source this load belongs to. * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not * belong to a specific media period. - * @param loadEventInfo The {@link LoadEventInfo} defining the load event. + * @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The values of {@link + * LoadEventInfo#elapsedRealtimeMs} and {@link LoadEventInfo#bytesLoaded} are relative to the + * corresponding {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)} + * event. * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. */ void onLoadCanceled( @@ -211,7 +230,10 @@ public interface MediaSourceEventListener { * @param windowIndex The window index in the timeline of the media source this load belongs to. * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not * belong to a specific media period. - * @param loadEventInfo The {@link LoadEventInfo} defining the load event. + * @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The values of {@link + * LoadEventInfo#elapsedRealtimeMs} and {@link LoadEventInfo#bytesLoaded} are relative to the + * corresponding {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)} + * event. * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. * @param error The load error. * @param wasCanceled Whether the load was canceled as a result of the error. @@ -268,7 +290,7 @@ public interface MediaSourceEventListener { /** Creates an event dispatcher. */ public EventDispatcher() { this( - /* listenerAndHandlers= */ new CopyOnWriteArrayList(), + /* listenerAndHandlers= */ new CopyOnWriteArrayList<>(), /* windowIndex= */ 0, /* mediaPeriodId= */ null, /* mediaTimeOffsetMs= */ 0); @@ -327,40 +349,31 @@ public interface MediaSourceEventListener { /** Dispatches {@link #onMediaPeriodCreated(int, MediaPeriodId)}. */ public void mediaPeriodCreated() { - Assertions.checkState(mediaPeriodId != null); + MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId); for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { final MediaSourceEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, - new Runnable() { - @Override - public void run() { - listener.onMediaPeriodCreated(windowIndex, mediaPeriodId); - } - }); + () -> listener.onMediaPeriodCreated(windowIndex, mediaPeriodId)); } } /** Dispatches {@link #onMediaPeriodReleased(int, MediaPeriodId)}. */ public void mediaPeriodReleased() { - Assertions.checkState(mediaPeriodId != null); + MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId); for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { final MediaSourceEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, - new Runnable() { - @Override - public void run() { - listener.onMediaPeriodReleased(windowIndex, mediaPeriodId); - } - }); + () -> listener.onMediaPeriodReleased(windowIndex, mediaPeriodId)); } } /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ - public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { + public void loadStarted(DataSpec dataSpec, Uri uri, int dataType, long elapsedRealtimeMs) { loadStarted( dataSpec, + uri, dataType, C.TRACK_TYPE_UNKNOWN, null, @@ -374,6 +387,7 @@ public interface MediaSourceEventListener { /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadStarted( DataSpec dataSpec, + Uri uri, int dataType, int trackType, @Nullable Format trackFormat, @@ -384,7 +398,7 @@ public interface MediaSourceEventListener { long elapsedRealtimeMs) { loadStarted( new LoadEventInfo( - dataSpec, elapsedRealtimeMs, /* loadDurationMs= */ 0, /* bytesLoaded= */ 0), + dataSpec, uri, elapsedRealtimeMs, /* loadDurationMs= */ 0, /* bytesLoaded= */ 0), new MediaLoadData( dataType, trackType, @@ -396,29 +410,26 @@ public interface MediaSourceEventListener { } /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ - public void loadStarted(final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) { + public void loadStarted(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { final MediaSourceEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, - new Runnable() { - @Override - public void run() { - listener.onLoadStarted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData); - } - }); + () -> listener.onLoadStarted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData)); } } /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadCompleted( DataSpec dataSpec, + Uri uri, int dataType, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) { loadCompleted( dataSpec, + uri, dataType, C.TRACK_TYPE_UNKNOWN, null, @@ -434,6 +445,7 @@ public interface MediaSourceEventListener { /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadCompleted( DataSpec dataSpec, + Uri uri, int dataType, int trackType, @Nullable Format trackFormat, @@ -445,7 +457,7 @@ public interface MediaSourceEventListener { long loadDurationMs, long bytesLoaded) { loadCompleted( - new LoadEventInfo(dataSpec, elapsedRealtimeMs, loadDurationMs, bytesLoaded), + new LoadEventInfo(dataSpec, uri, elapsedRealtimeMs, loadDurationMs, bytesLoaded), new MediaLoadData( dataType, trackType, @@ -457,30 +469,27 @@ public interface MediaSourceEventListener { } /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ - public void loadCompleted( - final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) { + public void loadCompleted(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { final MediaSourceEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, - new Runnable() { - @Override - public void run() { - listener.onLoadCompleted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData); - } - }); + () -> + listener.onLoadCompleted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData)); } } /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadCanceled( DataSpec dataSpec, + Uri uri, int dataType, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) { loadCanceled( dataSpec, + uri, dataType, C.TRACK_TYPE_UNKNOWN, null, @@ -496,6 +505,7 @@ public interface MediaSourceEventListener { /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadCanceled( DataSpec dataSpec, + Uri uri, int dataType, int trackType, @Nullable Format trackFormat, @@ -507,7 +517,7 @@ public interface MediaSourceEventListener { long loadDurationMs, long bytesLoaded) { loadCanceled( - new LoadEventInfo(dataSpec, elapsedRealtimeMs, loadDurationMs, bytesLoaded), + new LoadEventInfo(dataSpec, uri, elapsedRealtimeMs, loadDurationMs, bytesLoaded), new MediaLoadData( dataType, trackType, @@ -519,17 +529,13 @@ public interface MediaSourceEventListener { } /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ - public void loadCanceled(final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) { + public void loadCanceled(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { - final MediaSourceEventListener listener = listenerAndHandler.listener; + MediaSourceEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, - new Runnable() { - @Override - public void run() { - listener.onLoadCanceled(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData); - } - }); + () -> + listener.onLoadCanceled(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData)); } } @@ -539,6 +545,7 @@ public interface MediaSourceEventListener { */ public void loadError( DataSpec dataSpec, + Uri uri, int dataType, long elapsedRealtimeMs, long loadDurationMs, @@ -547,6 +554,7 @@ public interface MediaSourceEventListener { boolean wasCanceled) { loadError( dataSpec, + uri, dataType, C.TRACK_TYPE_UNKNOWN, null, @@ -567,6 +575,7 @@ public interface MediaSourceEventListener { */ public void loadError( DataSpec dataSpec, + Uri uri, int dataType, int trackType, @Nullable Format trackFormat, @@ -580,7 +589,7 @@ public interface MediaSourceEventListener { IOException error, boolean wasCanceled) { loadError( - new LoadEventInfo(dataSpec, elapsedRealtimeMs, loadDurationMs, bytesLoaded), + new LoadEventInfo(dataSpec, uri, elapsedRealtimeMs, loadDurationMs, bytesLoaded), new MediaLoadData( dataType, trackType, @@ -598,37 +607,28 @@ public interface MediaSourceEventListener { * boolean)}. */ public void loadError( - final LoadEventInfo loadEventInfo, - final MediaLoadData mediaLoadData, - final IOException error, - final boolean wasCanceled) { + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { final MediaSourceEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, - new Runnable() { - @Override - public void run() { + () -> listener.onLoadError( - windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData, error, wasCanceled); - } - }); + windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData, error, wasCanceled)); } } /** Dispatches {@link #onReadingStarted(int, MediaPeriodId)}. */ public void readingStarted() { - Assertions.checkState(mediaPeriodId != null); + MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId); for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { final MediaSourceEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, - new Runnable() { - @Override - public void run() { - listener.onReadingStarted(windowIndex, mediaPeriodId); - } - }); + () -> listener.onReadingStarted(windowIndex, mediaPeriodId)); } } @@ -646,17 +646,13 @@ public interface MediaSourceEventListener { } /** Dispatches {@link #onUpstreamDiscarded(int, MediaPeriodId, MediaLoadData)}. */ - public void upstreamDiscarded(final MediaLoadData mediaLoadData) { + public void upstreamDiscarded(MediaLoadData mediaLoadData) { + MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId); for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { final MediaSourceEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, - new Runnable() { - @Override - public void run() { - listener.onUpstreamDiscarded(windowIndex, mediaPeriodId, mediaLoadData); - } - }); + () -> listener.onUpstreamDiscarded(windowIndex, mediaPeriodId, mediaLoadData)); } } @@ -679,17 +675,12 @@ public interface MediaSourceEventListener { } /** Dispatches {@link #onDownstreamFormatChanged(int, MediaPeriodId, MediaLoadData)}. */ - public void downstreamFormatChanged(final MediaLoadData mediaLoadData) { + public void downstreamFormatChanged(MediaLoadData mediaLoadData) { for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { final MediaSourceEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, - new Runnable() { - @Override - public void run() { - listener.onDownstreamFormatChanged(windowIndex, mediaPeriodId, mediaLoadData); - } - }); + () -> listener.onDownstreamFormatChanged(windowIndex, mediaPeriodId, mediaLoadData)); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index f9bf86081f..d33cfb8abd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -20,6 +20,7 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -97,8 +98,11 @@ public final class MergingMediaSource extends CompositeMediaSource { } @Override - public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { - super.prepareSourceInternal(player, isTopLevelSource); + public void prepareSourceInternal( + ExoPlayer player, + boolean isTopLevelSource, + @Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(player, isTopLevelSource, mediaTransferListener); for (int i = 0; i < mediaSources.length; i++) { prepareChildSource(i, mediaSources[i]); } @@ -159,6 +163,12 @@ public final class MergingMediaSource extends CompositeMediaSource { } } + @Override + protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + Integer id, MediaPeriodId mediaPeriodId) { + return id == 0 ? mediaPeriodId : null; + } + private IllegalMergeException checkTimelineMerges(Timeline timeline) { if (periodCount == PERIOD_COUNT_UNSET) { periodCount = timeline.getPeriodCount(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index d9090baf3b..c378a8f9a2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -568,8 +568,12 @@ public final class SampleQueue implements TrackOutput { } @Override - public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - CryptoData cryptoData) { + public void sampleMetadata( + long timeUs, + @C.BufferFlags int flags, + int size, + int offset, + @Nullable CryptoData cryptoData) { if (pendingFormatAdjustment) { format(lastUnadjustedFormat); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index 0bddd482ac..9e33a2d898 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -193,4 +193,9 @@ public final class SinglePeriodTimeline extends Timeline { return UID.equals(uid) ? 0 : C.INDEX_UNSET; } + @Override + public Object getUidOfPeriod(int periodIndex) { + Assertions.checkIndex(periodIndex, 0, 1); + return UID; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 0a089e5b7c..458148499a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; @@ -24,8 +25,12 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispat import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.Loader; +import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; import com.google.android.exoplayer2.upstream.Loader.Loadable; +import com.google.android.exoplayer2.upstream.StatsDataSource; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -45,7 +50,8 @@ import java.util.Arrays; private final DataSpec dataSpec; private final DataSource.Factory dataSourceFactory; - private final int minLoadableRetryCount; + private final @Nullable TransferListener transferListener; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final TrackGroupArray tracks; private final ArrayList sampleStreams; @@ -61,21 +67,22 @@ import java.util.Arrays; /* package */ boolean loadingSucceeded; /* package */ byte[] sampleData; /* package */ int sampleSize; - private int errorCount; public SingleSampleMediaPeriod( DataSpec dataSpec, DataSource.Factory dataSourceFactory, + @Nullable TransferListener transferListener, Format format, long durationUs, - int minLoadableRetryCount, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, boolean treatLoadErrorsAsEndOfStream) { this.dataSpec = dataSpec; this.dataSourceFactory = dataSourceFactory; + this.transferListener = transferListener; this.format = format; this.durationUs = durationUs; - this.minLoadableRetryCount = minLoadableRetryCount; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; tracks = new TrackGroupArray(new TrackGroup(format)); @@ -137,13 +144,18 @@ import java.util.Arrays; if (loadingFinished || loader.isLoading()) { return false; } + DataSource dataSource = dataSourceFactory.createDataSource(); + if (transferListener != null) { + dataSource.addTransferListener(transferListener); + } long elapsedRealtimeMs = loader.startLoading( - new SourceLoadable(dataSpec, dataSourceFactory.createDataSource()), - this, - minLoadableRetryCount); + new SourceLoadable(dataSpec, dataSource), + /* callback= */ this, + loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MEDIA)); eventDispatcher.loadStarted( dataSpec, + dataSpec.uri, C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, format, @@ -192,8 +204,13 @@ import java.util.Arrays; @Override public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { + sampleSize = (int) loadable.dataSource.getBytesRead(); + sampleData = loadable.sampleData; + loadingFinished = true; + loadingSucceeded = true; eventDispatcher.loadCompleted( loadable.dataSpec, + loadable.dataSource.getLastOpenedUri(), C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, format, @@ -203,11 +220,7 @@ import java.util.Arrays; durationUs, elapsedRealtimeMs, loadDurationMs, - loadable.sampleSize); - sampleSize = loadable.sampleSize; - sampleData = loadable.sampleData; - loadingFinished = true; - loadingSucceeded = true; + sampleSize); } @Override @@ -215,6 +228,7 @@ import java.util.Arrays; boolean released) { eventDispatcher.loadCanceled( loadable.dataSpec, + loadable.dataSource.getLastOpenedUri(), C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, /* trackFormat= */ null, @@ -224,16 +238,37 @@ import java.util.Arrays; durationUs, elapsedRealtimeMs, loadDurationMs, - loadable.sampleSize); + loadable.dataSource.getBytesRead()); } @Override - public @Loader.RetryAction int onLoadError( - SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { - errorCount++; - boolean cancel = treatLoadErrorsAsEndOfStream && errorCount >= minLoadableRetryCount; + public LoadErrorAction onLoadError( + SourceLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error, + int errorCount) { + long retryDelay = + loadErrorHandlingPolicy.getRetryDelayMsFor( + C.DATA_TYPE_MEDIA, durationUs, error, errorCount); + boolean errorCanBePropagated = + retryDelay == C.TIME_UNSET + || errorCount + >= loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MEDIA); + + LoadErrorAction action; + if (treatLoadErrorsAsEndOfStream && errorCanBePropagated) { + loadingFinished = true; + action = Loader.DONT_RETRY; + } else { + action = + retryDelay != C.TIME_UNSET + ? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelay) + : Loader.DONT_RETRY_FATAL; + } eventDispatcher.loadError( loadable.dataSpec, + loadable.dataSource.getLastOpenedUri(), C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, format, @@ -243,14 +278,10 @@ import java.util.Arrays; durationUs, elapsedRealtimeMs, loadDurationMs, - loadable.sampleSize, + loadable.dataSource.getBytesRead(), error, - /* wasCanceled= */ cancel); - if (cancel) { - loadingFinished = true; - return Loader.DONT_RETRY; - } - return Loader.RETRY; + /* wasCanceled= */ !action.isRetry()); + return action; } private final class SampleStreamImpl implements SampleStream { @@ -333,14 +364,13 @@ import java.util.Arrays; public final DataSpec dataSpec; - private final DataSource dataSource; + private final StatsDataSource dataSource; - private int sampleSize; private byte[] sampleData; public SourceLoadable(DataSpec dataSpec, DataSource dataSource) { this.dataSpec = dataSpec; - this.dataSource = dataSource; + this.dataSource = new StatsDataSource(dataSource); } @Override @@ -348,22 +378,17 @@ import java.util.Arrays; // Never happens. } - @Override - public boolean isLoadCanceled() { - return false; - } - @Override public void load() throws IOException, InterruptedException { - // We always load from the beginning, so reset the sampleSize to 0. - sampleSize = 0; + // We always load from the beginning, so reset bytesRead to 0. + dataSource.resetBytesRead(); try { // Create and open the input. dataSource.open(dataSpec); // Load the sample data. int result = 0; while (result != C.RESULT_END_OF_INPUT) { - sampleSize += result; + int sampleSize = (int) dataSource.getBytesRead(); if (sampleData == null) { sampleData = new byte[INITIAL_SAMPLE_SIZE]; } else if (sampleSize == sampleData.length) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 2c651bef59..24f49cb086 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -24,6 +24,9 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -55,7 +58,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { private final DataSource.Factory dataSourceFactory; - private int minLoadableRetryCount; + private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private boolean treatLoadErrorsAsEndOfStream; private boolean isCreateCalled; private @Nullable Object tag; @@ -68,7 +71,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { */ public Factory(DataSource.Factory dataSourceFactory) { this.dataSourceFactory = Assertions.checkNotNull(dataSourceFactory); - this.minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; + loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); } /** @@ -86,16 +89,36 @@ public final class SingleSampleMediaSource extends BaseMediaSource { } /** - * Sets the minimum number of times to retry if a loading error occurs. The default value is - * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * Sets the minimum number of times to retry if a loading error occurs. See {@link + * #setLoadErrorHandlingPolicy} for the default value. + * + *

    Calling this method is equivalent to calling {@link #setLoadErrorHandlingPolicy} with + * {@link DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy(int) + * DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)} * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead. */ + @Deprecated public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)); + } + + /** + * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link + * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. + * + *

    Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}. + * + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { Assertions.checkState(!isCreateCalled); - this.minLoadableRetryCount = minLoadableRetryCount; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; return this; } @@ -130,7 +153,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { dataSourceFactory, format, durationUs, - minLoadableRetryCount, + loadErrorHandlingPolicy, treatLoadErrorsAsEndOfStream, tag); } @@ -155,19 +178,16 @@ public final class SingleSampleMediaSource extends BaseMediaSource { } - /** - * The default minimum number of times to retry loading data prior to failing. - */ - public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; - private final DataSpec dataSpec; private final DataSource.Factory dataSourceFactory; private final Format format; private final long durationUs; - private final int minLoadableRetryCount; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final boolean treatLoadErrorsAsEndOfStream; private final Timeline timeline; + private @Nullable TransferListener transferListener; + /** * @param uri The {@link Uri} of the media stream. * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will @@ -179,7 +199,12 @@ public final class SingleSampleMediaSource extends BaseMediaSource { @Deprecated public SingleSampleMediaSource( Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { - this(uri, dataSourceFactory, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT); + this( + uri, + dataSourceFactory, + format, + durationUs, + DefaultLoadErrorHandlingPolicy.DEFAULT_MIN_LOADABLE_RETRY_COUNT); } /** @@ -203,7 +228,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { dataSourceFactory, format, durationUs, - minLoadableRetryCount, + new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), /* treatLoadErrorsAsEndOfStream= */ false, /* tag= */ null); } @@ -239,7 +264,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { dataSourceFactory, format, durationUs, - minLoadableRetryCount, + new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), treatLoadErrorsAsEndOfStream, /* tag= */ null); if (eventHandler != null && eventListener != null) { @@ -252,13 +277,13 @@ public final class SingleSampleMediaSource extends BaseMediaSource { DataSource.Factory dataSourceFactory, Format format, long durationUs, - int minLoadableRetryCount, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, boolean treatLoadErrorsAsEndOfStream, @Nullable Object tag) { this.dataSourceFactory = dataSourceFactory; this.format = format; this.durationUs = durationUs; - this.minLoadableRetryCount = minLoadableRetryCount; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; dataSpec = new DataSpec(uri); timeline = @@ -268,7 +293,11 @@ public final class SingleSampleMediaSource extends BaseMediaSource { // MediaSource implementation. @Override - public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + public void prepareSourceInternal( + ExoPlayer player, + boolean isTopLevelSource, + @Nullable TransferListener mediaTransferListener) { + transferListener = mediaTransferListener; refreshSourceInfo(timeline, /* manifest= */ null); } @@ -283,9 +312,10 @@ public final class SingleSampleMediaSource extends BaseMediaSource { return new SingleSampleMediaPeriod( dataSpec, dataSourceFactory, + transferListener, format, durationUs, - minLoadableRetryCount, + loadErrorHandlingPolicy, createEventDispatcher(id), treatLoadErrorsAsEndOfStream); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java index a9fb261768..56c9989f34 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java @@ -72,11 +72,14 @@ public final class TrackGroup implements Parcelable { } /** - * Returns the index of the track with the given format in the group. + * Returns the index of the track with the given format in the group. The format is located by + * identity so, for example, {@code group.indexOf(group.getFormat(index)) == index} even if + * multiple tracks have formats that contain the same values. * * @param format The format. * @return The index of the track, or {@link C#INDEX_UNSET} if no such track exists. */ + @SuppressWarnings("ReferenceEquality") public int indexOf(Format format) { for (int i = 0; i < formats.length; i++) { if (format == formats[i]) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java index 8654e94bdb..53f0a418be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java @@ -96,6 +96,30 @@ public final class AdPlaybackState { return count == C.LENGTH_UNSET || getFirstAdIndexToPlay() < count; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AdGroup adGroup = (AdGroup) o; + return count == adGroup.count + && Arrays.equals(uris, adGroup.uris) + && Arrays.equals(states, adGroup.states) + && Arrays.equals(durationsUs, adGroup.durationsUs); + } + + @Override + public int hashCode() { + int result = count; + result = 31 * result + Arrays.hashCode(uris); + result = 31 * result + Arrays.hashCode(states); + result = 31 * result + Arrays.hashCode(durationsUs); + return result; + } + /** * Returns a new instance with the ad count set to {@code count}. This method may only be called * if this instance's ad count has not yet been specified. @@ -344,6 +368,14 @@ public final class AdPlaybackState { return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } + /** Returns an instance with the specified ad marked as skipped. */ + @CheckResult + public AdPlaybackState withSkippedAd(int adGroupIndex, int adIndexInAdGroup) { + AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_SKIPPED, adIndexInAdGroup); + return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); + } + /** Returns an instance with the specified ad marked as having a load error. */ @CheckResult public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) { @@ -393,4 +425,29 @@ public final class AdPlaybackState { } } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AdPlaybackState that = (AdPlaybackState) o; + return adGroupCount == that.adGroupCount + && adResumePositionUs == that.adResumePositionUs + && contentDurationUs == that.contentDurationUs + && Arrays.equals(adGroupTimesUs, that.adGroupTimesUs) + && Arrays.equals(adGroups, that.adGroups); + } + + @Override + public int hashCode() { + int result = adGroupCount; + result = 31 * result + (int) adResumePositionUs; + result = 31 * result + (int) contentDurationUs; + result = 31 * result + Arrays.hashCode(adGroupTimesUs); + result = 31 * result + Arrays.hashCode(adGroups); + return result; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 7f9dc18eaf..66370828b7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -36,6 +36,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadDa import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.lang.annotation.Retention; @@ -46,7 +47,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -/** A {@link MediaSource} that inserts ads linearly with a provided content media source. */ +/** + * A {@link MediaSource} that inserts ads linearly with a provided content media source. This source + * cannot be used as a child source in a composition. It must be the top-level source used to + * prepare the player. + */ public final class AdsMediaSource extends CompositeMediaSource { /** Factory for creating {@link MediaSource}s to play ad media. */ @@ -169,7 +174,9 @@ public final class AdsMediaSource extends CompositeMediaSource { } - private static final String TAG = "AdsMediaSource"; + // Used to identify the content "child" source for CompositeMediaSource. + private static final MediaPeriodId DUMMY_CONTENT_MEDIA_PERIOD_ID = + new MediaPeriodId(/* periodIndex= */ 0); private final MediaSource contentMediaSource; private final MediaSourceFactory adMediaSourceFactory; @@ -307,12 +314,17 @@ public final class AdsMediaSource extends CompositeMediaSource { } @Override - public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) { - super.prepareSourceInternal(player, isTopLevelSource); - Assertions.checkArgument(isTopLevelSource); + public void prepareSourceInternal( + final ExoPlayer player, + boolean isTopLevelSource, + @Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(player, isTopLevelSource, mediaTransferListener); + Assertions.checkArgument( + isTopLevelSource, + "AdsMediaSource must be the top-level source used to prepare the player."); final ComponentListener componentListener = new ComponentListener(); this.componentListener = componentListener; - prepareChildSource(new MediaPeriodId(/* periodIndex= */ 0), contentMediaSource); + prepareChildSource(DUMMY_CONTENT_MEDIA_PERIOD_ID, contentMediaSource); mainHandler.post(new Runnable() { @Override public void run() { @@ -338,20 +350,18 @@ public final class AdsMediaSource extends CompositeMediaSource { Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET); } adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource; - deferredMediaPeriodByAdMediaSource.put(adMediaSource, new ArrayList()); + deferredMediaPeriodByAdMediaSource.put(adMediaSource, new ArrayList<>()); prepareChildSource(id, adMediaSource); } MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; - DeferredMediaPeriod deferredMediaPeriod = - new DeferredMediaPeriod( - mediaSource, - new MediaPeriodId(/* periodIndex= */ 0, id.windowSequenceNumber), - allocator); + DeferredMediaPeriod deferredMediaPeriod = new DeferredMediaPeriod(mediaSource, id, allocator); deferredMediaPeriod.setPrepareErrorListener( new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup)); List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); if (mediaPeriods == null) { - deferredMediaPeriod.createPeriod(); + MediaPeriodId adSourceMediaPeriodId = + new MediaPeriodId(/* periodIndex= */ 0, id.windowSequenceNumber); + deferredMediaPeriod.createPeriod(adSourceMediaPeriodId); } else { // Keep track of the deferred media period so it can be populated with the real media period // when the source's info becomes available. @@ -360,7 +370,7 @@ public final class AdsMediaSource extends CompositeMediaSource { return deferredMediaPeriod; } else { DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(contentMediaSource, id, allocator); - mediaPeriod.createPeriod(); + mediaPeriod.createPeriod(id); return mediaPeriod; } } @@ -413,8 +423,8 @@ public final class AdsMediaSource extends CompositeMediaSource { @Override protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( MediaPeriodId childId, MediaPeriodId mediaPeriodId) { - // The child id for the content period is just a dummy without window sequence number. That's - // why we need to forward the reported mediaPeriodId in this case. + // The child id for the content period is just DUMMY_CONTENT_MEDIA_PERIOD_ID. That's why we need + // to forward the reported mediaPeriodId in this case. return childId.isAd() ? childId : mediaPeriodId; } @@ -441,12 +451,14 @@ public final class AdsMediaSource extends CompositeMediaSource { int adIndexInAdGroup, Timeline timeline) { Assertions.checkArgument(timeline.getPeriodCount() == 1); adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs(); - if (deferredMediaPeriodByAdMediaSource.containsKey(mediaSource)) { - List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); + List mediaPeriods = deferredMediaPeriodByAdMediaSource.remove(mediaSource); + if (mediaPeriods != null) { for (int i = 0; i < mediaPeriods.size(); i++) { - mediaPeriods.get(i).createPeriod(); + DeferredMediaPeriod mediaPeriod = mediaPeriods.get(i); + MediaPeriodId adSourceMediaPeriodId = + new MediaPeriodId(/* periodIndex= */ 0, mediaPeriod.id.windowSequenceNumber); + mediaPeriod.createPeriod(adSourceMediaPeriodId); } - deferredMediaPeriodByAdMediaSource.remove(mediaSource); } maybeUpdateSourceInfo(); } @@ -541,6 +553,7 @@ public final class AdsMediaSource extends CompositeMediaSource { createEventDispatcher(/* mediaPeriodId= */ null) .loadError( dataSpec, + dataSpec.uri, C.DATA_TYPE_AD, C.TRACK_TYPE_UNKNOWN, /* loadDurationMs= */ 0, @@ -582,6 +595,7 @@ public final class AdsMediaSource extends CompositeMediaSource { createEventDispatcher(mediaPeriodId) .loadError( new DataSpec(adUri), + adUri, C.DATA_TYPE_AD, C.TRACK_TYPE_UNKNOWN, /* loadDurationMs= */ 0, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java index b0c245b706..0594a635a5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java @@ -15,15 +15,15 @@ */ package com.google.android.exoplayer2.source.ads; +import android.support.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ForwardingTimeline; import com.google.android.exoplayer2.util.Assertions; -/** - * A {@link Timeline} for sources that have ads. - */ -/* package */ final class SinglePeriodAdTimeline extends ForwardingTimeline { +/** A {@link Timeline} for sources that have ads. */ +@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) +public final class SinglePeriodAdTimeline extends ForwardingTimeline { private final AdPlaybackState adPlaybackState; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java index e3eae2b4d8..e872f730de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java @@ -44,7 +44,7 @@ public abstract class BaseMediaChunk extends MediaChunk { * @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param seekTimeUs The media time from which output will begin, or {@link C#TIME_UNSET} if the * whole chunk should be output. - * @param chunkIndex The index of the chunk. + * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. */ public BaseMediaChunk( DataSource dataSource, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java new file mode 100644 index 0000000000..68dd322449 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.chunk; + +import java.util.NoSuchElementException; + +/** + * Base class for {@link MediaChunkIterator}s. Handles {@link #next()} and {@link #isEnded()}, and + * provides a bounds check for child classes. + */ +public abstract class BaseMediaChunkIterator implements MediaChunkIterator { + + private final long fromIndex; + private final long toIndex; + + private long currentIndex; + + /** + * Creates base iterator. + * + * @param fromIndex The index at which the iterator will start. + * @param toIndex The last available index. + */ + public BaseMediaChunkIterator(long fromIndex, long toIndex) { + this.fromIndex = fromIndex; + this.toIndex = toIndex; + currentIndex = fromIndex - 1; + } + + @Override + public boolean isEnded() { + return currentIndex > toIndex; + } + + @Override + public boolean next() { + currentIndex++; + return !isEnded(); + } + + /** + * Verifies that the iterator points to a valid element. + * + * @throws NoSuchElementException If the iterator does not point to a valid element. + */ + protected void checkInBounds() { + if (currentIndex < fromIndex || currentIndex > toIndex) { + throw new NoSuchElementException(); + } + } + + /** Returns the current index this iterator is pointing to. */ + protected long getCurrentIndex() { + return currentIndex; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java index 9531aaf32e..e0129e5c64 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java @@ -21,10 +21,8 @@ import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; -/** - * An output for {@link BaseMediaChunk}s. - */ -/* package */ final class BaseMediaChunkOutput implements TrackOutputProvider { +/** An output for {@link BaseMediaChunk}s. */ +public final class BaseMediaChunkOutput implements TrackOutputProvider { private static final String TAG = "BaseMediaChunkOutput"; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java index 0453a8fa12..91c3afec80 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java @@ -15,13 +15,17 @@ */ package com.google.android.exoplayer2.source.chunk; +import android.net.Uri; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.Loader.Loadable; +import com.google.android.exoplayer2.upstream.StatsDataSource; import com.google.android.exoplayer2.util.Assertions; +import java.util.List; +import java.util.Map; /** * An abstract base class for {@link Loadable} implementations that load chunks of data required @@ -64,7 +68,7 @@ public abstract class Chunk implements Loadable { */ public final long endTimeUs; - protected final DataSource dataSource; + protected final StatsDataSource dataSource; /** * @param dataSource The source from which the data should be loaded. @@ -85,7 +89,7 @@ public abstract class Chunk implements Loadable { @Nullable Object trackSelectionData, long startTimeUs, long endTimeUs) { - this.dataSource = Assertions.checkNotNull(dataSource); + this.dataSource = new StatsDataSource(dataSource); this.dataSpec = Assertions.checkNotNull(dataSpec); this.type = type; this.trackFormat = trackFormat; @@ -103,8 +107,31 @@ public abstract class Chunk implements Loadable { } /** - * Returns the number of bytes that have been loaded. + * Returns the number of bytes that have been loaded. Must only be called after the load + * completed, failed, or was canceled. */ - public abstract long bytesLoaded(); + public final long bytesLoaded() { + return dataSource.getBytesRead(); + } + /** + * Returns the {@link Uri} associated with the last {@link DataSource#open} call. If redirection + * occurred, this is the redirected uri. Must only be called after the load completed, failed, or + * was canceled. + * + * @see DataSource#getUri() + */ + public final Uri getUri() { + return dataSource.getLastOpenedUri(); + } + + /** + * Returns the response headers associated with the last {@link DataSource#open} call. Must only + * be called after the load completed, failed, or was canceled. + * + * @see DataSource#getResponseHeaders() + */ + public final Map> getResponseHeaders() { + return dataSource.getLastResponseHeaders(); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index f043571b69..a3abc75606 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import android.support.annotation.Nullable; import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -103,7 +104,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { * @param seekTimeUs The seek position within the new chunk, or {@link C#TIME_UNSET} to output the * whole chunk. */ - public void init(TrackOutputProvider trackOutputProvider, long seekTimeUs) { + public void init(@Nullable TrackOutputProvider trackOutputProvider, long seekTimeUs) { this.trackOutputProvider = trackOutputProvider; if (!extractorInitialized) { extractor.init(this); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 6cda68bac9..78914e9f33 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -27,7 +27,10 @@ import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.Loader; +import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -63,7 +66,7 @@ public class ChunkSampleStream implements SampleStream, S private final T chunkSource; private final SequenceableLoader.Callback> callback; private final EventDispatcher eventDispatcher; - private final int minLoadableRetryCount; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final Loader loader; private final ChunkHolder nextChunkHolder; private final ArrayList mediaChunks; @@ -80,6 +83,8 @@ public class ChunkSampleStream implements SampleStream, S /* package */ boolean loadingFinished; /** + * Constructs an instance. + * * @param primaryTrackType The type of the primary track. One of the {@link C} {@code * TRACK_TYPE_*} constants. * @param embeddedTrackTypes The types of any embedded tracks, or null. @@ -91,7 +96,10 @@ public class ChunkSampleStream implements SampleStream, S * @param minLoadableRetryCount The minimum number of times that the source should retry a load * before propagating an error. * @param eventDispatcher A dispatcher to notify of events. + * @deprecated Use {@link #ChunkSampleStream(int, int[], Format[], ChunkSource, Callback, + * Allocator, long, LoadErrorHandlingPolicy, EventDispatcher)} instead. */ + @Deprecated public ChunkSampleStream( int primaryTrackType, int[] embeddedTrackTypes, @@ -102,13 +110,49 @@ public class ChunkSampleStream implements SampleStream, S long positionUs, int minLoadableRetryCount, EventDispatcher eventDispatcher) { + this( + primaryTrackType, + embeddedTrackTypes, + embeddedTrackFormats, + chunkSource, + callback, + allocator, + positionUs, + new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), + eventDispatcher); + } + + /** + * Constructs an instance. + * + * @param primaryTrackType The type of the primary track. One of the {@link C} {@code + * TRACK_TYPE_*} constants. + * @param embeddedTrackTypes The types of any embedded tracks, or null. + * @param embeddedTrackFormats The formats of the embedded tracks, or null. + * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. + * @param callback An {@link Callback} for the stream. + * @param allocator An {@link Allocator} from which allocations can be obtained. + * @param positionUs The position from which to start loading media. + * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. + * @param eventDispatcher A dispatcher to notify of events. + */ + public ChunkSampleStream( + int primaryTrackType, + int[] embeddedTrackTypes, + Format[] embeddedTrackFormats, + T chunkSource, + Callback> callback, + Allocator allocator, + long positionUs, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, + EventDispatcher eventDispatcher) { this.primaryTrackType = primaryTrackType; this.embeddedTrackTypes = embeddedTrackTypes; this.embeddedTrackFormats = embeddedTrackFormats; this.chunkSource = chunkSource; this.callback = callback; this.eventDispatcher = eventDispatcher; - this.minLoadableRetryCount = minLoadableRetryCount; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; loader = new Loader("Loader:ChunkSampleStream"); nextChunkHolder = new ChunkHolder(); mediaChunks = new ArrayList<>(); @@ -385,9 +429,18 @@ public class ChunkSampleStream implements SampleStream, S @Override public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) { chunkSource.onChunkLoadCompleted(loadable); - eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, primaryTrackType, - loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, - loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, + eventDispatcher.loadCompleted( + loadable.dataSpec, + loadable.getUri(), + loadable.type, + primaryTrackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded()); callback.onContinueLoadingRequested(this); } @@ -395,9 +448,18 @@ public class ChunkSampleStream implements SampleStream, S @Override public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled(loadable.dataSpec, loadable.type, primaryTrackType, - loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, - loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, + eventDispatcher.loadCanceled( + loadable.dataSpec, + loadable.getUri(), + loadable.type, + primaryTrackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded()); if (!released) { primarySampleQueue.reset(); @@ -409,19 +471,26 @@ public class ChunkSampleStream implements SampleStream, S } @Override - public @Loader.RetryAction int onLoadError( - Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { + public LoadErrorAction onLoadError( + Chunk loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error, + int errorCount) { long bytesLoaded = loadable.bytesLoaded(); boolean isMediaChunk = isMediaChunk(loadable); int lastChunkIndex = mediaChunks.size() - 1; boolean cancelable = bytesLoaded == 0 || !isMediaChunk || !haveReadFromMediaChunk(lastChunkIndex); - boolean canceled = false; - if (chunkSource.onChunkLoadError(loadable, cancelable, error)) { - if (!cancelable) { - Log.w(TAG, "Ignoring attempt to cancel non-cancelable load."); - } else { - canceled = true; + long blacklistDurationMs = + cancelable + ? loadErrorHandlingPolicy.getBlacklistDurationMsFor( + loadable.type, loadDurationMs, error, errorCount) + : C.TIME_UNSET; + LoadErrorAction loadErrorAction = null; + if (chunkSource.onChunkLoadError(loadable, cancelable, error, blacklistDurationMs)) { + if (cancelable) { + loadErrorAction = Loader.DONT_RETRY; if (isMediaChunk) { BaseMediaChunk removed = discardUpstreamMediaChunksFromIndex(lastChunkIndex); Assertions.checkState(removed == loadable); @@ -429,18 +498,42 @@ public class ChunkSampleStream implements SampleStream, S pendingResetPositionUs = lastSeekPositionUs; } } + } else { + Log.w(TAG, "Ignoring attempt to cancel non-cancelable load."); } } - eventDispatcher.loadError(loadable.dataSpec, loadable.type, primaryTrackType, - loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, - loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, bytesLoaded, - error, canceled); + + if (loadErrorAction == null) { + // The load was not cancelled. Either the load must be retried or the error propagated. + long retryDelayMs = + loadErrorHandlingPolicy.getRetryDelayMsFor( + loadable.type, loadDurationMs, error, errorCount); + loadErrorAction = + retryDelayMs != C.TIME_UNSET + ? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs) + : Loader.DONT_RETRY_FATAL; + } + + boolean canceled = !loadErrorAction.isRetry(); + eventDispatcher.loadError( + loadable.dataSpec, + loadable.getUri(), + loadable.type, + primaryTrackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded, + error, + canceled); if (canceled) { callback.onContinueLoadingRequested(this); - return Loader.DONT_RETRY; - } else { - return Loader.RETRY; } + return loadErrorAction; } // SequenceableLoader implementation @@ -452,16 +545,16 @@ public class ChunkSampleStream implements SampleStream, S } boolean pendingReset = isPendingReset(); - MediaChunk previousChunk; + List chunkQueue; long loadPositionUs; if (pendingReset) { - previousChunk = null; + chunkQueue = Collections.emptyList(); loadPositionUs = pendingResetPositionUs; } else { - previousChunk = getLastMediaChunk(); - loadPositionUs = previousChunk.endTimeUs; + chunkQueue = readOnlyMediaChunks; + loadPositionUs = getLastMediaChunk().endTimeUs; } - chunkSource.getNextChunk(previousChunk, positionUs, loadPositionUs, nextChunkHolder); + chunkSource.getNextChunk(positionUs, loadPositionUs, chunkQueue, nextChunkHolder); boolean endOfStream = nextChunkHolder.endOfStream; Chunk loadable = nextChunkHolder.chunk; nextChunkHolder.clear(); @@ -487,10 +580,20 @@ public class ChunkSampleStream implements SampleStream, S mediaChunk.init(mediaChunkOutput); mediaChunks.add(mediaChunk); } - long elapsedRealtimeMs = loader.startLoading(loadable, this, minLoadableRetryCount); - eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, primaryTrackType, - loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, - loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs); + long elapsedRealtimeMs = + loader.startLoading( + loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)); + eventDispatcher.loadStarted( + loadable.dataSpec, + loadable.dataSpec.uri, + loadable.type, + primaryTrackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs); return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java index 568461c206..ee940954bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SeekParameters; import java.io.IOException; import java.util.List; @@ -59,29 +60,32 @@ public interface ChunkSource { /** * Returns the next chunk to load. - *

    - * If a chunk is available then {@link ChunkHolder#chunk} is set. If the end of the stream has + * + *

    If a chunk is available then {@link ChunkHolder#chunk} is set. If the end of the stream has * been reached then {@link ChunkHolder#endOfStream} is set. If a chunk is not available but the * end of the stream has not been reached, the {@link ChunkHolder} is not modified. * - * @param previous The most recently loaded media chunk. * @param playbackPositionUs The current playback position in microseconds. If playback of the * period to which this chunk source belongs has not yet started, the value will be the * starting position in the period minus the duration of any media in previous periods still * to be played. - * @param loadPositionUs The current load position in microseconds. If {@code previous} is null, + * @param loadPositionUs The current load position in microseconds. If {@code queue} is empty, * this is the starting position from which chunks should be provided. Else it's equal to - * {@code previous.endTimeUs}. + * {@link MediaChunk#endTimeUs} of the last chunk in the {@code queue}. + * @param queue The queue of buffered {@link MediaChunk}s. * @param out A holder to populate. */ - void getNextChunk(MediaChunk previous, long playbackPositionUs, long loadPositionUs, + void getNextChunk( + long playbackPositionUs, + long loadPositionUs, + List queue, ChunkHolder out); /** * Called when the {@link ChunkSampleStream} has finished loading a chunk obtained from this * source. - *

    - * This method should only be called when the source is enabled. + * + *

    This method should only be called when the source is enabled. * * @param chunk The chunk whose load has been completed. */ @@ -90,15 +94,15 @@ public interface ChunkSource { /** * Called when the {@link ChunkSampleStream} encounters an error loading a chunk obtained from * this source. - *

    - * This method should only be called when the source is enabled. + * + *

    This method should only be called when the source is enabled. * * @param chunk The chunk whose load encountered the error. * @param cancelable Whether the load can be canceled. * @param e The error. - * @return Whether the load should be canceled. Should always be false if {@code cancelable} is - * false. + * @param blacklistDurationMs The duration for which the associated track may be blacklisted, or + * {@link C#TIME_UNSET} if the track may not be blacklisted. + * @return Whether the load should be canceled. Must be false if {@code cancelable} is false. */ - boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e); - + boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e, long blacklistDurationMs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java deleted file mode 100644 index 38e0c0d51f..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.source.chunk; - -import android.util.Log; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException; - -/** - * Helper class for blacklisting tracks in a {@link TrackSelection} when 404 (Not Found) and 410 - * (Gone) HTTP response codes are encountered. - */ -public final class ChunkedTrackBlacklistUtil { - - /** - * The default duration for which a track is blacklisted in milliseconds. - */ - public static final long DEFAULT_TRACK_BLACKLIST_MS = 60000; - - private static final String TAG = "ChunkedTrackBlacklist"; - - /** - * Blacklists {@code trackSelectionIndex} in {@code trackSelection} for - * {@link #DEFAULT_TRACK_BLACKLIST_MS} if {@code e} is an {@link InvalidResponseCodeException} - * with {@link InvalidResponseCodeException#responseCode} equal to 404 or 410. Else does nothing. - * Note that blacklisting will fail if the track is the only non-blacklisted track in the - * selection. - * - * @param trackSelection The track selection. - * @param trackSelectionIndex The index in the selection to consider blacklisting. - * @param e The error to inspect. - * @return Whether the track was blacklisted in the selection. - */ - public static boolean maybeBlacklistTrack(TrackSelection trackSelection, int trackSelectionIndex, - Exception e) { - return maybeBlacklistTrack(trackSelection, trackSelectionIndex, e, DEFAULT_TRACK_BLACKLIST_MS); - } - - /** - * Blacklists {@code trackSelectionIndex} in {@code trackSelection} for - * {@code blacklistDurationMs} if calling {@link #shouldBlacklist(Exception)} for {@code e} - * returns true. Else does nothing. Note that blacklisting will fail if the track is the only - * non-blacklisted track in the selection. - * - * @param trackSelection The track selection. - * @param trackSelectionIndex The index in the selection to consider blacklisting. - * @param e The error to inspect. - * @param blacklistDurationMs The duration to blacklist the track for, if it is blacklisted. - * @return Whether the track was blacklisted. - */ - public static boolean maybeBlacklistTrack(TrackSelection trackSelection, int trackSelectionIndex, - Exception e, long blacklistDurationMs) { - if (shouldBlacklist(e)) { - boolean blacklisted = trackSelection.blacklist(trackSelectionIndex, blacklistDurationMs); - int responseCode = ((InvalidResponseCodeException) e).responseCode; - if (blacklisted) { - Log.w(TAG, "Blacklisted: duration=" + blacklistDurationMs + ", responseCode=" - + responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex)); - } else { - Log.w(TAG, "Blacklisting failed (cannot blacklist last enabled track): responseCode=" - + responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex)); - } - return blacklisted; - } - return false; - } - - /** - * Returns whether a loading error is an {@link InvalidResponseCodeException} with - * {@link InvalidResponseCodeException#responseCode} equal to 404 or 410. - * - * @param e The loading error. - * @return Wheter the loading error is an {@link InvalidResponseCodeException} with - * {@link InvalidResponseCodeException#responseCode} equal to 404 or 410. - */ - public static boolean shouldBlacklist(Exception e) { - if (e instanceof InvalidResponseCodeException) { - int responseCode = ((InvalidResponseCodeException) e).responseCode; - return responseCode == 404 || responseCode == 410; - } - return false; - } - - private ChunkedTrackBlacklistUtil() {} - -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java index ed73cf2588..2d5ba3d2e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; @@ -31,13 +32,15 @@ import java.io.IOException; */ public class ContainerMediaChunk extends BaseMediaChunk { + private static final PositionHolder DUMMY_POSITION_HOLDER = new PositionHolder(); + private final int chunkCount; private final long sampleOffsetUs; private final ChunkExtractorWrapper extractorWrapper; - private volatile int bytesLoaded; + private long nextLoadPosition; private volatile boolean loadCanceled; - private volatile boolean loadCompleted; + private boolean loadCompleted; /** * @param dataSource The source from which the data should be loaded. @@ -49,7 +52,7 @@ public class ContainerMediaChunk extends BaseMediaChunk { * @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param seekTimeUs The media time from which output will begin, or {@link C#TIME_UNSET} if the * whole chunk should be output. - * @param chunkIndex The index of the chunk. + * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. * @param chunkCount The number of chunks in the underlying media that are spanned by this * instance. Normally equal to one, but may be larger if multiple chunks as defined by the * underlying media are being merged into a single load. @@ -94,11 +97,6 @@ public class ContainerMediaChunk extends BaseMediaChunk { return loadCompleted; } - @Override - public final long bytesLoaded() { - return bytesLoaded; - } - // Loadable implementation. @Override @@ -106,20 +104,15 @@ public class ContainerMediaChunk extends BaseMediaChunk { loadCanceled = true; } - @Override - public final boolean isLoadCanceled() { - return loadCanceled; - } - @SuppressWarnings("NonAtomicVolatileUpdate") @Override public final void load() throws IOException, InterruptedException { - DataSpec loadDataSpec = dataSpec.subrange(bytesLoaded); + DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); try { // Create and open the input. ExtractorInput input = new DefaultExtractorInput(dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); - if (bytesLoaded == 0) { + if (nextLoadPosition == 0) { // Configure the output and set it as the target for the extractor wrapper. BaseMediaChunkOutput output = getOutput(); output.setSampleOffsetUs(sampleOffsetUs); @@ -131,11 +124,11 @@ public class ContainerMediaChunk extends BaseMediaChunk { Extractor extractor = extractorWrapper.extractor; int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractor.read(input, null); + result = extractor.read(input, DUMMY_POSITION_HOLDER); } Assertions.checkState(result != Extractor.RESULT_SEEK); } finally { - bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); + nextLoadPosition = input.getPosition() - dataSpec.absoluteStreamPosition; } } finally { Util.closeQuietly(dataSource); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java index 0846e7679d..7ea2521eb2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java @@ -32,7 +32,6 @@ public abstract class DataChunk extends Chunk { private static final int READ_GRANULARITY = 16 * 1024; private byte[] data; - private int limit; private volatile boolean loadCanceled; @@ -63,11 +62,6 @@ public abstract class DataChunk extends Chunk { return data; } - @Override - public long bytesLoaded() { - return limit; - } - // Loadable implementation @Override @@ -75,19 +69,14 @@ public abstract class DataChunk extends Chunk { loadCanceled = true; } - @Override - public final boolean isLoadCanceled() { - return loadCanceled; - } - @Override public final void load() throws IOException, InterruptedException { try { dataSource.open(dataSpec); - limit = 0; + int limit = 0; int bytesRead = 0; while (bytesRead != C.RESULT_END_OF_INPUT && !loadCanceled) { - maybeExpandData(); + maybeExpandData(limit); bytesRead = dataSource.read(data, limit, READ_GRANULARITY); if (bytesRead != -1) { limit += bytesRead; @@ -111,7 +100,7 @@ public abstract class DataChunk extends Chunk { */ protected abstract void consume(byte[] data, int limit) throws IOException; - private void maybeExpandData() { + private void maybeExpandData(int limit) { if (data == null) { data = new byte[READ_GRANULARITY]; } else if (data.length < limit + READ_GRANULARITY) { @@ -120,5 +109,4 @@ public abstract class DataChunk extends Chunk { data = Arrays.copyOf(data, data.length + READ_GRANULARITY); } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java index 6dd90b8735..d5c0d6f301 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; @@ -32,9 +33,11 @@ import java.io.IOException; */ public final class InitializationChunk extends Chunk { + private static final PositionHolder DUMMY_POSITION_HOLDER = new PositionHolder(); + private final ChunkExtractorWrapper extractorWrapper; - private volatile int bytesLoaded; + private long nextLoadPosition; private volatile boolean loadCanceled; /** @@ -57,11 +60,6 @@ public final class InitializationChunk extends Chunk { this.extractorWrapper = extractorWrapper; } - @Override - public long bytesLoaded() { - return bytesLoaded; - } - // Loadable implementation. @Override @@ -69,20 +67,15 @@ public final class InitializationChunk extends Chunk { loadCanceled = true; } - @Override - public boolean isLoadCanceled() { - return loadCanceled; - } - @SuppressWarnings("NonAtomicVolatileUpdate") @Override public void load() throws IOException, InterruptedException { - DataSpec loadDataSpec = dataSpec.subrange(bytesLoaded); + DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); try { // Create and open the input. ExtractorInput input = new DefaultExtractorInput(dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); - if (bytesLoaded == 0) { + if (nextLoadPosition == 0) { extractorWrapper.init(/* trackOutputProvider= */ null, C.TIME_UNSET); } // Load and decode the initialization data. @@ -90,11 +83,11 @@ public final class InitializationChunk extends Chunk { Extractor extractor = extractorWrapper.extractor; int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractor.read(input, null); + result = extractor.read(input, DUMMY_POSITION_HOLDER); } Assertions.checkState(result != Extractor.RESULT_SEEK); } finally { - bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); + nextLoadPosition = input.getPosition() - dataSpec.absoluteStreamPosition; } } finally { Util.closeQuietly(dataSource); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java index d313a8cb81..9626f4b03f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.util.Assertions; */ public abstract class MediaChunk extends Chunk { - /** The chunk index. */ + /** The chunk index, or {@link C#INDEX_UNSET} if it is not known. */ public final long chunkIndex; /** @@ -37,7 +37,7 @@ public abstract class MediaChunk extends Chunk { * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. - * @param chunkIndex The index of the chunk. + * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. */ public MediaChunk( DataSource dataSource, @@ -54,9 +54,9 @@ public abstract class MediaChunk extends Chunk { this.chunkIndex = chunkIndex; } - /** Returns the next chunk index. */ + /** Returns the next chunk index or {@link C#INDEX_UNSET} if it is not known. */ public long getNextChunkIndex() { - return chunkIndex + 1; + return chunkIndex != C.INDEX_UNSET ? chunkIndex + 1 : C.INDEX_UNSET; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java new file mode 100644 index 0000000000..71d8940e26 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.chunk; + +import com.google.android.exoplayer2.upstream.DataSpec; +import java.util.NoSuchElementException; + +/** + * Iterator for media chunk sequences. + * + *

    The iterator initially points in front of the first available element. The first call to + * {@link #next()} moves the iterator to the first element. Check the return value of {@link + * #next()} or {@link #isEnded()} to determine whether the iterator reached the end of the available + * data. + */ +public interface MediaChunkIterator { + + /** An empty media chunk iterator without available data. */ + MediaChunkIterator EMPTY = + new MediaChunkIterator() { + @Override + public boolean isEnded() { + return true; + } + + @Override + public boolean next() { + return false; + } + + @Override + public DataSpec getDataSpec() { + throw new NoSuchElementException(); + } + + @Override + public long getChunkStartTimeUs() { + throw new NoSuchElementException(); + } + + @Override + public long getChunkEndTimeUs() { + throw new NoSuchElementException(); + } + }; + + /** Returns whether the iteration has reached the end of the available data. */ + boolean isEnded(); + + /** + * Moves the iterator to the next media chunk. + * + *

    Check the return value or {@link #isEnded()} to determine whether the iterator reached the + * end of the available data. + * + * @return Whether the iterator points to a media chunk with available data. + */ + boolean next(); + + /** + * Returns the {@link DataSpec} used to load the media chunk. + * + * @throws java.util.NoSuchElementException If the method is called before the first call to + * {@link #next()} or when {@link #isEnded()} is true. + */ + DataSpec getDataSpec(); + + /** + * Returns the media start time of the chunk, in microseconds. + * + * @throws java.util.NoSuchElementException If the method is called before the first call to + * {@link #next()} or when {@link #isEnded()} is true. + */ + long getChunkStartTimeUs(); + + /** + * Returns the media end time of the chunk, in microseconds. + * + * @throws java.util.NoSuchElementException If the method is called before the first call to + * {@link #next()} or when {@link #isEnded()} is true. + */ + long getChunkEndTimeUs(); +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java index bd2363ede1..2c00c7690d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java @@ -33,9 +33,8 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { private final int trackType; private final Format sampleFormat; - private volatile int bytesLoaded; - private volatile boolean loadCanceled; - private volatile boolean loadCompleted; + private long nextLoadPosition; + private boolean loadCompleted; /** * @param dataSource The source from which the data should be loaded. @@ -45,7 +44,7 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. - * @param chunkIndex The index of the chunk. + * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. * @param trackType The type of the chunk. Typically one of the {@link C} {@code TRACK_TYPE_*} * constants. * @param sampleFormat The {@link Format} of the sample in the chunk. @@ -81,34 +80,25 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { return loadCompleted; } - @Override - public long bytesLoaded() { - return bytesLoaded; - } - // Loadable implementation. @Override public void cancelLoad() { - loadCanceled = true; - } - - @Override - public boolean isLoadCanceled() { - return loadCanceled; + // Do nothing. } @SuppressWarnings("NonAtomicVolatileUpdate") @Override public void load() throws IOException, InterruptedException { - DataSpec loadDataSpec = dataSpec.subrange(bytesLoaded); + DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); try { // Create and open the input. long length = dataSource.open(loadDataSpec); if (length != C.LENGTH_UNSET) { - length += bytesLoaded; + length += nextLoadPosition; } - ExtractorInput extractorInput = new DefaultExtractorInput(dataSource, bytesLoaded, length); + ExtractorInput extractorInput = + new DefaultExtractorInput(dataSource, nextLoadPosition, length); BaseMediaChunkOutput output = getOutput(); output.setSampleOffsetUs(0); TrackOutput trackOutput = output.track(0, trackType); @@ -116,10 +106,10 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { // Load the sample data. int result = 0; while (result != C.RESULT_END_OF_INPUT) { - bytesLoaded += result; + nextLoadPosition += result; result = trackOutput.sampleData(extractorInput, Integer.MAX_VALUE, true); } - int sampleSize = bytesLoaded; + int sampleSize = (int) nextLoadPosition; trackOutput.sampleMetadata(startTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); } finally { Util.closeQuietly(dataSource); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index 139e403844..a64a1835d8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -107,7 +107,7 @@ public interface SubtitleDecoderFactory { case MimeTypes.APPLICATION_MP4CEA608: return new Cea608Decoder(format.sampleMimeType, format.accessibilityChannel); case MimeTypes.APPLICATION_CEA708: - return new Cea708Decoder(format.accessibilityChannel); + return new Cea708Decoder(format.accessibilityChannel, format.initializationData); case MimeTypes.APPLICATION_DVBSUBS: return new DvbDecoder(format.initializationData); case MimeTypes.APPLICATION_PGS: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index c6d7f6f163..5b74bd1505 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -20,6 +20,7 @@ import android.os.Handler.Callback; import android.os.Looper; import android.os.Message; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -27,6 +28,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; @@ -70,7 +72,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { private static final int MSG_UPDATE_OUTPUT = 0; - private final Handler outputHandler; + private final @Nullable Handler outputHandler; private final TextOutput output; private final SubtitleDecoderFactory decoderFactory; private final FormatHolder formatHolder; @@ -87,30 +89,31 @@ public final class TextRenderer extends BaseRenderer implements Callback { /** * @param output The output. - * @param outputLooper The looper associated with the thread on which the output should be - * called. If the output makes use of standard Android UI components, then this should - * normally be the looper associated with the application's main thread, which can be obtained - * using {@link android.app.Activity#getMainLooper()}. Null may be passed if the output - * should be called directly on the player's internal rendering thread. + * @param outputLooper The looper associated with the thread on which the output should be called. + * If the output makes use of standard Android UI components, then this should normally be the + * looper associated with the application's main thread, which can be obtained using {@link + * android.app.Activity#getMainLooper()}. Null may be passed if the output should be called + * directly on the player's internal rendering thread. */ - public TextRenderer(TextOutput output, Looper outputLooper) { + public TextRenderer(TextOutput output, @Nullable Looper outputLooper) { this(output, outputLooper, SubtitleDecoderFactory.DEFAULT); } /** * @param output The output. - * @param outputLooper The looper associated with the thread on which the output should be - * called. If the output makes use of standard Android UI components, then this should - * normally be the looper associated with the application's main thread, which can be obtained - * using {@link android.app.Activity#getMainLooper()}. Null may be passed if the output - * should be called directly on the player's internal rendering thread. + * @param outputLooper The looper associated with the thread on which the output should be called. + * If the output makes use of standard Android UI components, then this should normally be the + * looper associated with the application's main thread, which can be obtained using {@link + * android.app.Activity#getMainLooper()}. Null may be passed if the output should be called + * directly on the player's internal rendering thread. * @param decoderFactory A factory from which to obtain {@link SubtitleDecoder} instances. */ - public TextRenderer(TextOutput output, Looper outputLooper, - SubtitleDecoderFactory decoderFactory) { + public TextRenderer( + TextOutput output, @Nullable Looper outputLooper, SubtitleDecoderFactory decoderFactory) { super(C.TRACK_TYPE_TEXT); this.output = Assertions.checkNotNull(output); - this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this); + this.outputHandler = + outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this); this.decoderFactory = decoderFactory; formatHolder = new FormatHolder(); } @@ -305,7 +308,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { } private void clearOutput() { - updateOutput(Collections.emptyList()); + updateOutput(Collections.emptyList()); } @SuppressWarnings("unchecked") diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index f018e055fb..725321e53f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -21,10 +21,10 @@ import android.text.Layout.Alignment; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; -import android.text.style.CharacterStyle; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; @@ -55,15 +55,13 @@ public final class Cea608Decoder extends CeaDecoder { private static final int[] ROW_INDICES = new int[] {11, 1, 3, 12, 14, 5, 7, 9}; private static final int[] COLUMN_INDICES = new int[] {0, 4, 8, 12, 16, 20, 24, 28}; - private static final int[] COLORS = new int[] { - Color.WHITE, - Color.GREEN, - Color.BLUE, - Color.CYAN, - Color.RED, - Color.YELLOW, - Color.MAGENTA, - }; + + private static final int[] STYLE_COLORS = + new int[] { + Color.WHITE, Color.GREEN, Color.BLUE, Color.CYAN, Color.RED, Color.YELLOW, Color.MAGENTA + }; + private static final int STYLE_ITALICS = 0x07; + private static final int STYLE_UNCHANGED = 0x08; // The default number of rows to display in roll-up captions mode. private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4; @@ -374,18 +372,13 @@ public final class Cea608Decoder extends CeaDecoder { private void handleMidrowCtrl(byte cc2) { // TODO: support the extended styles (i.e. backgrounds and transparencies) - // cc2 - 0|0|1|0|ATRBT|U - // ATRBT is the 3-byte encoded attribute, and U is the underline toggle - boolean isUnderlined = (cc2 & 0x01) == 0x01; - currentCueBuilder.setUnderline(isUnderlined); + // A midrow control code advances the cursor. + currentCueBuilder.append(' '); - int attribute = (cc2 >> 1) & 0x0F; - if (attribute == 0x07) { - currentCueBuilder.setMidrowStyle(new StyleSpan(Typeface.ITALIC), 2); - currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(Color.WHITE), 1); - } else { - currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(COLORS[attribute]), 1); - } + // cc2 - 0|0|1|0|STYLE|U + boolean underline = (cc2 & 0x01) == 0x01; + int style = (cc2 >> 1) & 0x07; + currentCueBuilder.setStyle(style, underline); } private void handlePreambleAddressCode(byte cc1, byte cc2) { @@ -411,22 +404,18 @@ public final class Cea608Decoder extends CeaDecoder { currentCueBuilder.setRow(row); } - if ((cc2 & 0x01) == 0x01) { - currentCueBuilder.setPreambleStyle(new UnderlineSpan()); - } - // cc2 - 0|1|N|0|STYLE|U // cc2 - 0|1|N|1|CURSR|U - int attribute = cc2 >> 1 & 0x0F; - if (attribute <= 0x07) { - if (attribute == 0x07) { - currentCueBuilder.setPreambleStyle(new StyleSpan(Typeface.ITALIC)); - currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(Color.WHITE)); - } else { - currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(COLORS[attribute])); - } - } else { - currentCueBuilder.setIndent(COLUMN_INDICES[attribute & 0x07]); + boolean isCursor = (cc2 & 0x10) == 0x10; + boolean underline = (cc2 & 0x01) == 0x01; + int cursorOrStyle = (cc2 >> 1) & 0x07; + + // We need to call setStyle even for the isCursor case, to update the underline bit. + // STYLE_UNCHANGED is used for this case. + currentCueBuilder.setStyle(isCursor ? STYLE_UNCHANGED : cursorOrStyle, underline); + + if (isCursor) { + currentCueBuilder.setIndent(COLUMN_INDICES[cursorOrStyle]); } } @@ -582,44 +571,37 @@ public final class Cea608Decoder extends CeaDecoder { private static class CueBuilder { - private static final int POSITION_UNSET = -1; - // 608 captions define a 15 row by 32 column screen grid. These constants convert from 608 // positions to normalized screen position. private static final int SCREEN_CHARWIDTH = 32; private static final int BASE_ROW = 15; - private final List preambleStyles; - private final List midrowStyles; + private final List cueStyles; private final List rolledUpCaptions; - private final SpannableStringBuilder captionStringBuilder; + private final StringBuilder captionStringBuilder; private int row; private int indent; private int tabOffset; private int captionMode; private int captionRowCount; - private int underlineStartPosition; public CueBuilder(int captionMode, int captionRowCount) { - preambleStyles = new ArrayList<>(); - midrowStyles = new ArrayList<>(); + cueStyles = new ArrayList<>(); rolledUpCaptions = new ArrayList<>(); - captionStringBuilder = new SpannableStringBuilder(); + captionStringBuilder = new StringBuilder(); reset(captionMode); setCaptionRowCount(captionRowCount); } public void reset(int captionMode) { this.captionMode = captionMode; - preambleStyles.clear(); - midrowStyles.clear(); + cueStyles.clear(); rolledUpCaptions.clear(); - captionStringBuilder.clear(); + captionStringBuilder.setLength(0); row = BASE_ROW; indent = 0; tabOffset = 0; - underlineStartPosition = POSITION_UNSET; } public void setCaptionRowCount(int captionRowCount) { @@ -627,7 +609,8 @@ public final class Cea608Decoder extends CeaDecoder { } public boolean isEmpty() { - return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty() + return cueStyles.isEmpty() + && rolledUpCaptions.isEmpty() && captionStringBuilder.length() == 0; } @@ -635,6 +618,16 @@ public final class Cea608Decoder extends CeaDecoder { int length = captionStringBuilder.length(); if (length > 0) { captionStringBuilder.delete(length - 1, length); + // Decrement style start positions if necessary. + for (int i = cueStyles.size() - 1; i >= 0; i--) { + CueStyle style = cueStyles.get(i); + if (style.start == length) { + style.start--; + } else { + // All earlier cues must have style.start < length. + break; + } + } } } @@ -648,11 +641,8 @@ public final class Cea608Decoder extends CeaDecoder { public void rollUp() { rolledUpCaptions.add(buildSpannableString()); - captionStringBuilder.clear(); - preambleStyles.clear(); - midrowStyles.clear(); - underlineStartPosition = POSITION_UNSET; - + captionStringBuilder.setLength(0); + cueStyles.clear(); int numRows = Math.min(captionRowCount, row); while (rolledUpCaptions.size() >= numRows) { rolledUpCaptions.remove(0); @@ -667,23 +657,8 @@ public final class Cea608Decoder extends CeaDecoder { tabOffset = tabs; } - public void setPreambleStyle(CharacterStyle style) { - preambleStyles.add(style); - } - - public void setMidrowStyle(CharacterStyle style, int nextStyleIncrement) { - midrowStyles.add(new CueStyle(style, captionStringBuilder.length(), nextStyleIncrement)); - } - - public void setUnderline(boolean enabled) { - if (enabled) { - underlineStartPosition = captionStringBuilder.length(); - } else if (underlineStartPosition != POSITION_UNSET) { - // underline spans won't overlap, so it's safe to modify the builder directly with them - captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, - captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - underlineStartPosition = POSITION_UNSET; - } + public void setStyle(int style, boolean underline) { + cueStyles.add(new CueStyle(style, underline, captionStringBuilder.length())); } public void append(char text) { @@ -691,31 +666,69 @@ public final class Cea608Decoder extends CeaDecoder { } public SpannableString buildSpannableString() { - int length = captionStringBuilder.length(); + SpannableStringBuilder builder = new SpannableStringBuilder(captionStringBuilder); + int length = builder.length(); - // preamble styles apply to the entire cue - for (int i = 0; i < preambleStyles.size(); i++) { - captionStringBuilder.setSpan(preambleStyles.get(i), 0, length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + int underlineStartPosition = C.INDEX_UNSET; + int italicStartPosition = C.INDEX_UNSET; + int colorStartPosition = 0; + int color = Color.WHITE; + + boolean nextItalic = false; + int nextColor = Color.WHITE; + + for (int i = 0; i < cueStyles.size(); i++) { + CueStyle cueStyle = cueStyles.get(i); + boolean underline = cueStyle.underline; + int style = cueStyle.style; + if (style != STYLE_UNCHANGED) { + // If the style is a color then italic is cleared. + nextItalic = style == STYLE_ITALICS; + // If the style is italic then the color is left unchanged. + nextColor = style == STYLE_ITALICS ? nextColor : STYLE_COLORS[style]; + } + + int position = cueStyle.start; + int nextPosition = (i + 1) < cueStyles.size() ? cueStyles.get(i + 1).start : length; + if (position == nextPosition) { + // There are more cueStyles to process at the current position. + continue; + } + + // Process changes to underline up to the current position. + if (underlineStartPosition != C.INDEX_UNSET && !underline) { + setUnderlineSpan(builder, underlineStartPosition, position); + underlineStartPosition = C.INDEX_UNSET; + } else if (underlineStartPosition == C.INDEX_UNSET && underline) { + underlineStartPosition = position; + } + // Process changes to italic up to the current position. + if (italicStartPosition != C.INDEX_UNSET && !nextItalic) { + setItalicSpan(builder, italicStartPosition, position); + italicStartPosition = C.INDEX_UNSET; + } else if (italicStartPosition == C.INDEX_UNSET && nextItalic) { + italicStartPosition = position; + } + // Process changes to color up to the current position. + if (nextColor != color) { + setColorSpan(builder, colorStartPosition, position, color); + color = nextColor; + colorStartPosition = position; + } } - // midrow styles only apply to part of the cue, and after preamble styles - for (int i = 0; i < midrowStyles.size(); i++) { - CueStyle cueStyle = midrowStyles.get(i); - int end = (i < midrowStyles.size() - cueStyle.nextStyleIncrement) - ? midrowStyles.get(i + cueStyle.nextStyleIncrement).start - : length; - captionStringBuilder.setSpan(cueStyle.style, cueStyle.start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + // Add any final spans. + if (underlineStartPosition != C.INDEX_UNSET && underlineStartPosition != length) { + setUnderlineSpan(builder, underlineStartPosition, length); + } + if (italicStartPosition != C.INDEX_UNSET && italicStartPosition != length) { + setItalicSpan(builder, italicStartPosition, length); + } + if (colorStartPosition != length) { + setColorSpan(builder, colorStartPosition, length, color); } - // special case for midrow underlines that went to the end of the cue - if (underlineStartPosition != POSITION_UNSET) { - captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - return new SpannableString(captionStringBuilder); + return new SpannableString(builder); } public Cue build() { @@ -785,16 +798,34 @@ public final class Cea608Decoder extends CeaDecoder { return captionStringBuilder.toString(); } + private static void setUnderlineSpan(SpannableStringBuilder builder, int start, int end) { + builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private static void setItalicSpan(SpannableStringBuilder builder, int start, int end) { + builder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private static void setColorSpan( + SpannableStringBuilder builder, int start, int end, int color) { + if (color == Color.WHITE) { + // White is treated as the default color (i.e. no span is attached). + return; + } + builder.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + private static class CueStyle { - public final CharacterStyle style; - public final int start; - public final int nextStyleIncrement; + public final int style; + public final boolean underline; - public CueStyle(CharacterStyle style, int start, int nextStyleIncrement) { + public int start; + + public CueStyle(int style, boolean underline, int start) { this.style = style; + this.underline = underline; this.start = start; - this.nextStyleIncrement = nextStyleIncrement; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index 6bdbebc73b..f21804b01b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -38,7 +38,6 @@ import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; /** @@ -153,10 +152,10 @@ public final class Cea708Decoder extends CeaDecoder { private DtvCcPacket currentDtvCcPacket; private int currentWindow; - public Cea708Decoder(int accessibilityChannel) { + public Cea708Decoder(int accessibilityChannel, List initializationData) { ccData = new ParsableByteArray(); serviceBlockPacket = new ParsableBitArray(); - selectedServiceNumber = (accessibilityChannel == Format.NO_VALUE) ? 1 : accessibilityChannel; + selectedServiceNumber = accessibilityChannel == Format.NO_VALUE ? 1 : accessibilityChannel; cueBuilders = new CueBuilder[NUM_WINDOWS]; for (int i = 0; i < NUM_WINDOWS; i++) { @@ -196,7 +195,10 @@ public final class Cea708Decoder extends CeaDecoder { @Override protected void decode(SubtitleInputBuffer inputBuffer) { - ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit()); + // Subtitle input buffers are non-direct and the position is zero, so calling array() is safe. + @SuppressWarnings("ByteBufferBackingArray") + byte[] inputBufferData = inputBuffer.data.array(); + ccData.reset(inputBufferData, inputBuffer.data.limit()); while (ccData.bytesLeft() >= 3) { int ccTypeAndValid = (ccData.readUnsignedByte() & 0x07); @@ -741,7 +743,7 @@ public final class Cea708Decoder extends CeaDecoder { } } Collections.sort(displayCues); - return Collections.unmodifiableList(displayCues); + return Collections.unmodifiableList(displayCues); } private void resetCueBuilders() { @@ -879,7 +881,7 @@ public final class Cea708Decoder extends CeaDecoder { private int row; public CueBuilder() { - rolledUpCaptions = new LinkedList<>(); + rolledUpCaptions = new ArrayList<>(); captionStringBuilder = new SpannableStringBuilder(); reset(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java new file mode 100644 index 0000000000..10bed14adc --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.text.cea; + +import java.util.Collections; +import java.util.List; + +/** Initialization data for CEA-708 decoders. */ +public final class Cea708InitializationData { + + /** + * Whether the closed caption service is formatted for displays with 16:9 aspect ratio. If false, + * the closed caption service is formatted for 4:3 displays. + */ + public final boolean isWideAspectRatio; + + private Cea708InitializationData(List initializationData) { + isWideAspectRatio = initializationData.get(0)[0] != 0; + } + + /** + * Returns an object representation of CEA-708 initialization data + * + * @param initializationData Binary CEA-708 initialization data. + * @return The object representation. + */ + public static Cea708InitializationData fromData(List initializationData) { + return new Cea708InitializationData(initializationData); + } + + /** + * Builds binary CEA-708 initialization data. + * + * @param isWideAspectRatio Whether the closed caption service is formatted for displays with 16:9 + * aspect ratio. + * @return Binary CEA-708 initializaton data. + */ + public static List buildData(boolean isWideAspectRatio) { + return Collections.singletonList(new byte[] {(byte) (isWideAspectRatio ? 1 : 0)}); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java index 07a55f1a40..3efc16bdd0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java @@ -24,7 +24,7 @@ import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.text.SubtitleInputBuffer; import com.google.android.exoplayer2.text.SubtitleOutputBuffer; import com.google.android.exoplayer2.util.Assertions; -import java.util.LinkedList; +import java.util.ArrayDeque; import java.util.PriorityQueue; /** @@ -35,8 +35,8 @@ import java.util.PriorityQueue; private static final int NUM_INPUT_BUFFERS = 10; private static final int NUM_OUTPUT_BUFFERS = 2; - private final LinkedList availableInputBuffers; - private final LinkedList availableOutputBuffers; + private final ArrayDeque availableInputBuffers; + private final ArrayDeque availableOutputBuffers; private final PriorityQueue queuedInputBuffers; private CeaInputBuffer dequeuedInputBuffer; @@ -44,11 +44,11 @@ import java.util.PriorityQueue; private long queuedInputBufferCount; public CeaDecoder() { - availableInputBuffers = new LinkedList<>(); + availableInputBuffers = new ArrayDeque<>(); for (int i = 0; i < NUM_INPUT_BUFFERS; i++) { availableInputBuffers.add(new CeaInputBuffer()); } - availableOutputBuffers = new LinkedList<>(); + availableOutputBuffers = new ArrayDeque<>(); for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) { availableOutputBuffers.add(new CeaOutputBuffer()); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java index 7da2054a08..738f251e27 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java @@ -54,7 +54,7 @@ import java.util.List; @Override public List getCues(long timeUs) { - return timeUs >= 0 ? cues : Collections.emptyList(); + return timeUs >= 0 ? cues : Collections.emptyList(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java index 67271ee218..911feadb56 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java @@ -26,13 +26,13 @@ public final class CeaUtil { private static final String TAG = "CeaUtil"; + public static final int USER_DATA_IDENTIFIER_GA94 = Util.getIntegerCodeForString("GA94"); + public static final int USER_DATA_TYPE_CODE_MPEG_CC = 0x3; + private static final int PAYLOAD_TYPE_CC = 4; private static final int COUNTRY_CODE = 0xB5; private static final int PROVIDER_CODE_ATSC = 0x31; private static final int PROVIDER_CODE_DIRECTV = 0x2F; - private static final int USER_ID_GA94 = Util.getIntegerCodeForString("GA94"); - private static final int USER_ID_DTG1 = Util.getIntegerCodeForString("DTG1"); - private static final int USER_DATA_TYPE_CODE = 0x3; /** * Consumes the unescaped content of an SEI NAL unit, writing the content of any CEA-608 messages @@ -67,32 +67,52 @@ public final class CeaUtil { boolean messageIsSupportedCeaCaption = countryCode == COUNTRY_CODE && (providerCode == PROVIDER_CODE_ATSC || providerCode == PROVIDER_CODE_DIRECTV) - && userDataTypeCode == USER_DATA_TYPE_CODE; + && userDataTypeCode == USER_DATA_TYPE_CODE_MPEG_CC; if (providerCode == PROVIDER_CODE_ATSC) { - messageIsSupportedCeaCaption &= - userIdentifier == USER_ID_GA94 || userIdentifier == USER_ID_DTG1; + messageIsSupportedCeaCaption &= userIdentifier == USER_DATA_IDENTIFIER_GA94; } if (messageIsSupportedCeaCaption) { - // Ignore first three bits: reserved (1) + process_cc_data_flag (1) + zero_bit (1). - int ccCount = seiBuffer.readUnsignedByte() & 0x1F; - // Ignore em_data (1) - seiBuffer.skipBytes(1); - // Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2) - // + cc_data_1 (8) + cc_data_2 (8). - int sampleLength = ccCount * 3; - int sampleStartPosition = seiBuffer.getPosition(); - for (TrackOutput output : outputs) { - seiBuffer.setPosition(sampleStartPosition); - output.sampleData(seiBuffer, sampleLength); - output.sampleMetadata( - presentationTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null); - } + consumeCcData(presentationTimeUs, seiBuffer, outputs); } } seiBuffer.setPosition(nextPayloadPosition); } } + /** + * Consumes caption data (cc_data), writing the content as samples to all of the provided outputs. + * + * @param presentationTimeUs The presentation time in microseconds for any samples. + * @param ccDataBuffer The buffer containing the caption data. + * @param outputs The outputs to which any samples should be written. + */ + public static void consumeCcData( + long presentationTimeUs, ParsableByteArray ccDataBuffer, TrackOutput[] outputs) { + // First byte contains: reserved (1), process_cc_data_flag (1), zero_bit (1), cc_count (5). + int firstByte = ccDataBuffer.readUnsignedByte(); + boolean processCcDataFlag = (firstByte & 0x40) != 0; + if (!processCcDataFlag) { + // No need to process. + return; + } + int ccCount = firstByte & 0x1F; + ccDataBuffer.skipBytes(1); // Ignore em_data + // Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2) + // + cc_data_1 (8) + cc_data_2 (8). + int sampleLength = ccCount * 3; + int sampleStartPosition = ccDataBuffer.getPosition(); + for (TrackOutput output : outputs) { + ccDataBuffer.setPosition(sampleStartPosition); + output.sampleData(ccDataBuffer, sampleLength); + output.sampleMetadata( + presentationTimeUs, + C.BUFFER_FLAG_KEY_FRAME, + sampleLength, + /* offset= */ 0, + /* encryptionData= */ null); + } + } + /** * Reads a value from the provided buffer consisting of zero or more 0xFF bytes followed by a * terminating byte not equal to 0xFF. The returned value is ((0xFF * N) + T), where N is the diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java index 1e45595144..091bda49f3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java @@ -25,7 +25,6 @@ import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.zip.DataFormatException; import java.util.zip.Inflater; /** A {@link SimpleSubtitleDecoder} for PGS subtitles. */ @@ -39,25 +38,22 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { private static final byte INFLATE_HEADER = 0x78; private final ParsableByteArray buffer; + private final ParsableByteArray inflatedBuffer; private final CueBuilder cueBuilder; private Inflater inflater; - private byte[] inflatedData; - private int inflatedDataSize; public PgsDecoder() { super("PgsDecoder"); buffer = new ParsableByteArray(); + inflatedBuffer = new ParsableByteArray(); cueBuilder = new CueBuilder(); } @Override protected Subtitle decode(byte[] data, int size, boolean reset) throws SubtitleDecoderException { - if (maybeInflateData(data, size)) { - buffer.reset(inflatedData, inflatedDataSize); - } else { - buffer.reset(data, size); - } + buffer.reset(data, size); + maybeInflateData(buffer); cueBuilder.reset(); ArrayList cues = new ArrayList<>(); while (buffer.bytesLeft() >= 3) { @@ -69,31 +65,14 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { return new PgsSubtitle(Collections.unmodifiableList(cues)); } - private boolean maybeInflateData(byte[] data, int size) { - if (size == 0 || data[0] != INFLATE_HEADER) { - return false; - } - if (inflater == null) { - inflater = new Inflater(); - inflatedData = new byte[size]; - } - inflatedDataSize = 0; - inflater.setInput(data, 0, size); - try { - while (!inflater.finished() && !inflater.needsDictionary() && !inflater.needsInput()) { - if (inflatedDataSize == inflatedData.length) { - inflatedData = Arrays.copyOf(inflatedData, inflatedData.length * 2); - } - inflatedDataSize += - inflater.inflate( - inflatedData, inflatedDataSize, inflatedData.length - inflatedDataSize); + private void maybeInflateData(ParsableByteArray buffer) { + if (buffer.bytesLeft() > 0 && buffer.peekUnsignedByte() == INFLATE_HEADER) { + if (inflater == null) { + inflater = new Inflater(); } - return inflater.finished(); - } catch (DataFormatException e) { - // Assume data is not compressed. - return false; - } finally { - inflater.reset(); + if (Util.inflate(buffer, inflatedBuffer, inflater)) { + buffer.reset(inflatedBuffer.data, inflatedBuffer.limit()); + } // else assume data is not compressed. } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index 0cb6f66898..e528a57762 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -62,7 +62,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { super("SsaDecoder"); if (initializationData != null && !initializationData.isEmpty()) { haveInitializationData = true; - String formatLine = new String(initializationData.get(0)); + String formatLine = Util.fromUtf8Bytes(initializationData.get(0)); Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); parseFormatLine(formatLine); parseHeader(new ParsableByteArray(initializationData.get(1))); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java index ad8f849c60..61e0085065 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java @@ -26,8 +26,8 @@ import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.XmlPullParserUtil; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.ArrayDeque; import java.util.HashMap; -import java.util.LinkedList; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -109,13 +109,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length); xmlParser.setInput(inputStream, null); TtmlSubtitle ttmlSubtitle = null; - LinkedList nodeStack = new LinkedList<>(); + ArrayDeque nodeStack = new ArrayDeque<>(); int unsupportedNodeDepth = 0; int eventType = xmlParser.getEventType(); FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE; CellResolution cellResolution = DEFAULT_CELL_RESOLUTION; while (eventType != XmlPullParser.END_DOCUMENT) { - TtmlNode parent = nodeStack.peekLast(); + TtmlNode parent = nodeStack.peek(); if (unsupportedNodeDepth == 0) { String name = xmlParser.getName(); if (eventType == XmlPullParser.START_TAG) { @@ -131,7 +131,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } else { try { TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate); - nodeStack.addLast(node); + nodeStack.push(node); if (parent != null) { parent.addChild(node); } @@ -145,9 +145,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { parent.addChild(TtmlNode.buildTextNode(xmlParser.getText())); } else if (eventType == XmlPullParser.END_TAG) { if (xmlParser.getName().equals(TtmlNode.TAG_TT)) { - ttmlSubtitle = new TtmlSubtitle(nodeStack.getLast(), globalStyles, regionMap); + ttmlSubtitle = new TtmlSubtitle(nodeStack.peek(), globalStyles, regionMap); } - nodeStack.removeLast(); + nodeStack.pop(); } } else { if (eventType == XmlPullParser.START_TAG) { @@ -178,7 +178,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { float frameRateMultiplier = 1; String frameRateMultiplierString = xmlParser.getAttributeValue(TTP, "frameRateMultiplier"); if (frameRateMultiplierString != null) { - String[] parts = frameRateMultiplierString.split(" "); + String[] parts = Util.split(frameRateMultiplierString, " "); if (parts.length != 2) { throw new SubtitleDecoderException("frameRateMultiplier doesn't have 2 parts"); } @@ -354,7 +354,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } private String[] parseStyleIds(String parentStyleIds) { - return parentStyleIds.split("\\s+"); + parentStyleIds = parentStyleIds.trim(); + return parentStyleIds.isEmpty() ? new String[0] : Util.split(parentStyleIds, "\\s+"); } private TtmlStyle parseStyleAttributes(XmlPullParser parser, TtmlStyle style) { @@ -531,7 +532,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { private static void parseFontSize(String expression, TtmlStyle out) throws SubtitleDecoderException { - String[] expressions = expression.split("\\s+"); + String[] expressions = Util.split(expression, "\\s+"); Matcher matcher; if (expressions.length == 1) { matcher = FONT_SIZE.matcher(expression); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java index 433436f771..50916aa841 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java @@ -37,8 +37,8 @@ import java.util.Map; Map regionMap) { this.root = root; this.regionMap = regionMap; - this.globalStyles = globalStyles != null - ? Collections.unmodifiableMap(globalStyles) : Collections.emptyMap(); + this.globalStyles = + globalStyles != null ? Collections.unmodifiableMap(globalStyles) : Collections.emptyMap(); this.eventTimesUs = root.getEventTimesUs(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java index 2270ccc632..ebc38bcd70 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java @@ -92,7 +92,8 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { | ((initializationBytes[27] & 0xFF) << 16) | ((initializationBytes[28] & 0xFF) << 8) | (initializationBytes[29] & 0xFF); - String fontFamily = new String(initializationBytes, 43, initializationBytes.length - 43); + String fontFamily = + Util.fromUtf8Bytes(initializationBytes, 43, initializationBytes.length - 43); defaultFontFamily = TX3G_SERIF.equals(fontFamily) ? C.SERIF_NAME : C.SANS_SERIF_NAME; //font size (initializationBytes[25]) is 5% of video height calculatedVideoTrackHeight = 20 * initializationBytes[25]; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java index 4f2fc8373e..adb1190ce4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java @@ -57,7 +57,7 @@ import java.util.List; @Override public List getCues(long timeUs) { - return timeUs >= 0 ? cues : Collections.emptyList(); + return timeUs >= 0 ? cues : Collections.emptyList(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java index ea1e6891f0..81c362bda5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.text.webvtt; import android.text.TextUtils; import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -314,7 +315,7 @@ import java.util.regex.Pattern; } selector = selector.substring(0, voiceStartIndex); } - String[] classDivision = selector.split("\\."); + String[] classDivision = Util.split(selector, "\\."); String tagAndIdDivision = classDivision[0]; int idPrefixIndex = tagAndIdDivision.indexOf('#'); if (idPrefixIndex != -1) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java index 159dd4f2e0..8cb0ac58c7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java @@ -78,14 +78,14 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { int boxType = sampleData.readInt(); remainingCueBoxBytes -= BOX_HEADER_SIZE; int payloadLength = boxSize - BOX_HEADER_SIZE; - String boxPayload = new String(sampleData.data, sampleData.getPosition(), payloadLength); + String boxPayload = + Util.fromUtf8Bytes(sampleData.data, sampleData.getPosition(), payloadLength); sampleData.skipBytes(payloadLength); remainingCueBoxBytes -= payloadLength; if (boxType == TYPE_sttg) { WebvttCueParser.parseCueSettingsList(boxPayload, builder); } else if (boxType == TYPE_payl) { - WebvttCueParser.parseCueText(null, boxPayload.trim(), builder, - Collections.emptyList()); + WebvttCueParser.parseCueText(null, boxPayload.trim(), builder, Collections.emptyList()); } else { // Other VTTCueBox children are still not supported and are ignored. } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java index 881300807e..c87c88133c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java @@ -51,6 +51,6 @@ import java.util.List; @Override public List getCues(long timeUs) { - return timeUs >= 0 ? cues : Collections.emptyList(); + return timeUs >= 0 ? cues : Collections.emptyList(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index 80ebecdc0e..6f2a1328c6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -34,11 +34,12 @@ import android.text.style.UnderlineSpan; import android.util.Log; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -157,7 +158,7 @@ public final class WebvttCueParser { /* package */ static void parseCueText(String id, String markup, WebvttCue.Builder builder, List styles) { SpannableStringBuilder spannedText = new SpannableStringBuilder(); - Stack startTagStack = new Stack<>(); + ArrayDeque startTagStack = new ArrayDeque<>(); List scratchStyleMatches = new ArrayList<>(); int pos = 0; while (pos < markup.length()) { @@ -456,7 +457,7 @@ public final class WebvttCueParser { if (tagExpression.isEmpty()) { return null; } - return tagExpression.split("[ \\.]")[0]; + return Util.splitAtFirst(tagExpression, "[ \\.]")[0]; } private static void getApplicableStyles(List declaredStyles, String id, @@ -518,7 +519,7 @@ public final class WebvttCueParser { voice = fullTagExpression.substring(voiceStartIndex).trim(); fullTagExpression = fullTagExpression.substring(0, voiceStartIndex); } - String[] nameAndClasses = fullTagExpression.split("\\."); + String[] nameAndClasses = Util.split(fullTagExpression, "\\."); String name = nameAndClasses[0]; String[] classes; if (nameAndClasses.length > 1) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java index d0c3eda494..b94be19d8f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.text.webvtt; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -53,8 +54,8 @@ public final class WebvttParserUtil { */ public static long parseTimestampUs(String timestamp) throws NumberFormatException { long value = 0; - String[] parts = timestamp.split("\\.", 2); - String[] subparts = parts[0].split(":"); + String[] parts = Util.splitAtFirst(timestamp, "\\."); + String[] subparts = Util.split(parts[0], ":"); for (String subpart : subparts) { value = (value * 60) + Long.parseLong(subpart); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index b28dc6ca6f..64b0da281c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.trackselection; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; @@ -35,7 +36,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { */ public static final class Factory implements TrackSelection.Factory { - private final BandwidthMeter bandwidthMeter; + private final @Nullable BandwidthMeter bandwidthMeter; private final int minDurationForQualityIncreaseMs; private final int maxDurationForQualityDecreaseMs; private final int minDurationToRetainAfterDiscardMs; @@ -44,9 +45,24 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final long minTimeBetweenBufferReevaluationMs; private final Clock clock; + /** Creates an adaptive track selection factory with default parameters. */ + public Factory() { + this( + DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, + DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, + DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + DEFAULT_BANDWIDTH_FRACTION, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); + } + /** - * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + * @deprecated Use {@link #Factory()} instead. Custom bandwidth meter should be directly passed + * to the player in ExoPlayerFactory. */ + @Deprecated + @SuppressWarnings("deprecation") public Factory(BandwidthMeter bandwidthMeter) { this( bandwidthMeter, @@ -60,7 +76,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { } /** - * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + * Creates an adaptive track selection factory. + * * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the * selected track to switch to one of higher quality. * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the @@ -73,6 +90,27 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { * consider available for use. Setting to a value less than 1 is recommended to account for * inaccuracies in the bandwidth estimator. */ + public Factory( + int minDurationForQualityIncreaseMs, + int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, + float bandwidthFraction) { + this( + minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, + minDurationToRetainAfterDiscardMs, + bandwidthFraction, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); + } + + /** + * @deprecated Use {@link #Factory(int, int, int, float)} instead. Custom bandwidth meter should + * be directly passed to the player in ExoPlayerFactory. + */ + @Deprecated + @SuppressWarnings("deprecation") public Factory( BandwidthMeter bandwidthMeter, int minDurationForQualityIncreaseMs, @@ -91,7 +129,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { } /** - * @param bandwidthMeter Provides an estimate of the currently available bandwidth.. + * Creates an adaptive track selection factory. + * * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the * selected track to switch to one of higher quality. * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the @@ -115,8 +154,33 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { * buffer reevaluation calls. * @param clock A {@link Clock}. */ + @SuppressWarnings("deprecation") public Factory( - BandwidthMeter bandwidthMeter, + int minDurationForQualityIncreaseMs, + int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, + float bandwidthFraction, + float bufferedFractionToLiveEdgeForQualityIncrease, + long minTimeBetweenBufferReevaluationMs, + Clock clock) { + this( + /* bandwidthMeter= */ null, + minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, + minDurationToRetainAfterDiscardMs, + bandwidthFraction, + bufferedFractionToLiveEdgeForQualityIncrease, + minTimeBetweenBufferReevaluationMs, + clock); + } + + /** + * @deprecated Use {@link #Factory(int, int, int, float, float, long, Clock)} instead. Custom + * bandwidth meter should be directly passed to the player in ExoPlayerFactory. + */ + @Deprecated + public Factory( + @Nullable BandwidthMeter bandwidthMeter, int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, int minDurationToRetainAfterDiscardMs, @@ -136,7 +200,11 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { } @Override - public AdaptiveTrackSelection createTrackSelection(TrackGroup group, int... tracks) { + public AdaptiveTrackSelection createTrackSelection( + TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) { + if (this.bandwidthMeter != null) { + bandwidthMeter = this.bandwidthMeter; + } return new AdaptiveTrackSelection( group, tracks, @@ -242,9 +310,11 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs; this.clock = clock; playbackSpeed = 1f; - selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE); reason = C.SELECTION_REASON_INITIAL; lastBufferEvaluationMs = C.TIME_UNSET; + @SuppressWarnings("nullness:method.invocation.invalid") + int selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE); + this.selectedIndex = selectedIndex; } @Override @@ -301,7 +371,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { } @Override - public Object getSelectionData() { + public @Nullable Object getSelectionData() { return null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java index 81eb5dd888..3f201bccea 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java @@ -110,6 +110,7 @@ public abstract class BaseTrackSelection implements TrackSelection { } @Override + @SuppressWarnings("ReferenceEquality") public final int indexOf(Format format) { for (int i = 0; i < length; i++) { if (formats[i] == format) { @@ -183,7 +184,9 @@ public abstract class BaseTrackSelection implements TrackSelection { return hashCode; } + // Track groups are compared by identity not value, as distinct groups may have the same value. @Override + @SuppressWarnings("ReferenceEquality") public boolean equals(@Nullable Object obj) { if (this == obj) { return true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 71d2544784..58784e4c5a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -19,7 +19,6 @@ import android.content.Context; import android.graphics.Point; import android.os.Parcel; import android.os.Parcelable; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Pair; @@ -44,6 +43,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A default {@link TrackSelector} suitable for most use cases. Track selections are made according @@ -56,7 +56,7 @@ import java.util.concurrent.atomic.AtomicReference; * obtain a {@link ParametersBuilder} initialized with the current {@link Parameters}. The desired * modifications can be made on the builder, and the resulting {@link Parameters} can then be built * and set on the selector. For example the following code modifies the parameters to restrict video - * track selections to SD, and to prefer German audio tracks: + * track selections to SD, and to select a German audio track if there is one: * *

    {@code
      * // Build on the current parameters.
    @@ -83,7 +83,7 @@ import java.util.concurrent.atomic.AtomicReference;
      *
      * Selection {@link Parameters} support many different options, some of which are described below.
      *
    - * 

    Track selection overrides

    + *

    Selecting specific tracks

    * * Track selection overrides can be used to select specific tracks. To specify an override for a * renderer, it's first necessary to obtain the tracks that have been mapped to it: @@ -110,13 +110,6 @@ import java.util.concurrent.atomic.AtomicReference; * .setSelectionOverride(rendererIndex, rendererTrackGroups, selectionOverride)); * }
    * - *

    Disabling renderers

    - * - * Renderers can be disabled using {@link ParametersBuilder#setRendererDisabled}. Disabling a - * renderer differs from setting a {@code null} override because the renderer is disabled - * unconditionally, whereas a {@code null} override is applied only when the track groups available - * to the renderer match the {@link TrackGroupArray} for which it was specified. - * *

    Constraint based track selection

    * * Whilst track selection overrides make it possible to select specific tracks, the recommended way @@ -145,6 +138,13 @@ import java.util.concurrent.atomic.AtomicReference; * only applied to periods whose tracks match those for which the override was set. * * + *

    Disabling renderers

    + * + * Renderers can be disabled using {@link ParametersBuilder#setRendererDisabled}. Disabling a + * renderer differs from setting a {@code null} override because the renderer is disabled + * unconditionally, whereas a {@code null} override is applied only when the track groups available + * to the renderer match the {@link TrackGroupArray} for which it was specified. + * *

    Tunneling

    * * Tunneled playback can be enabled in cases where the combination of renderers and selected tracks @@ -154,15 +154,16 @@ import java.util.concurrent.atomic.AtomicReference; public class DefaultTrackSelector extends MappingTrackSelector { /** - * A builder for {@link Parameters}. + * A builder for {@link Parameters}. See the {@link Parameters} documentation for explanations of + * the parameters that can be configured using this builder. */ public static final class ParametersBuilder { private final SparseArray> selectionOverrides; private final SparseBooleanArray rendererDisabledFlags; - private String preferredAudioLanguage; - private String preferredTextLanguage; + private @Nullable String preferredAudioLanguage; + private @Nullable String preferredTextLanguage; private boolean selectUndeterminedTextLanguage; private int disabledTextTrackSelectionFlags; private boolean forceLowestBitrate; @@ -178,9 +179,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private boolean viewportOrientationMayChange; private int tunnelingAudioSessionId; - /** - * Creates a builder obtaining the initial values from {@link Parameters#DEFAULT}. - */ + /** Creates a builder with default initial values. */ public ParametersBuilder() { this(Parameters.DEFAULT); } @@ -344,15 +343,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Equivalent to invoking {@link #setViewportSize} with the viewport size obtained from - * {@link Util#getPhysicalDisplaySize(Context)}. + * Equivalent to calling {@link #setViewportSize(int, int, boolean)} with the viewport size + * obtained from {@link Util#getPhysicalDisplaySize(Context)}. * - * @param context The context to obtain the viewport size from. - * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange}. + * @param context Any context. + * @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}. * @return This builder. */ - public ParametersBuilder setViewportSizeToPhysicalDisplaySize(Context context, - boolean viewportOrientationMayChange) { + public ParametersBuilder setViewportSizeToPhysicalDisplaySize( + Context context, boolean viewportOrientationMayChange) { // Assume the viewport is fullscreen. Point viewportSize = Util.getPhysicalDisplaySize(context); return setViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange); @@ -369,13 +368,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and - * {@link Parameters#viewportOrientationMayChange}. + * See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and {@link + * Parameters#viewportOrientationMayChange}. * + * @param viewportWidth See {@link Parameters#viewportWidth}. + * @param viewportHeight See {@link Parameters#viewportHeight}. + * @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}. * @return This builder. */ - public ParametersBuilder setViewportSize(int viewportWidth, int viewportHeight, - boolean viewportOrientationMayChange) { + public ParametersBuilder setViewportSize( + int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) { this.viewportWidth = viewportWidth; this.viewportHeight = viewportHeight; this.viewportOrientationMayChange = viewportOrientationMayChange; @@ -486,8 +488,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Enables or disables tunneling. To enable tunneling, pass an audio session id to use when in - * tunneling mode. Session ids can be generated using {@link + * See {@link Parameters#tunnelingAudioSessionId}. + * + *

    Enables or disables tunneling. To enable tunneling, pass an audio session id to use when + * in tunneling mode. Session ids can be generated using {@link * C#generateAudioSessionIdV21(Context)}. To disable tunneling pass {@link * C#AUDIO_SESSION_ID_UNSET}. Tunneling will only be activated if it's both enabled and * supported by the audio and video renderers for the selected tracks. @@ -541,25 +545,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** Constraint parameters for {@link DefaultTrackSelector}. */ public static final class Parameters implements Parcelable { - /** - * An instance with default values: - * - *

      - *
    • No preferred audio language. - *
    • No preferred text language. - *
    • Text tracks with undetermined language are not selected if no track with {@link - * #preferredTextLanguage} is available. - *
    • All selection flags are considered for text track selections. - *
    • Lowest bitrate track selections are not forced. - *
    • Adaptation between different mime types is not allowed. - *
    • Non seamless adaptation is allowed. - *
    • No max limit for video width/height. - *
    • No max video bitrate. - *
    • Video constraints are exceeded if no supported selection can be made otherwise. - *
    • Renderer capabilities are exceeded if no supported selection can be made. - *
    • No viewport constraints. - *
    - */ + /** An instance with default values. */ public static final Parameters DEFAULT = new Parameters(); // Per renderer overrides. @@ -569,112 +555,138 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Audio /** - * The preferred language for audio, as well as for forced text tracks, as an ISO 639-2/T tag. - * {@code null} selects the default track, or the first track if there's no default. + * The preferred language for audio and forced text tracks, as an ISO 639-2/T tag. {@code null} + * selects the default track, or the first track if there's no default. The default value is + * {@code null}. */ - public final String preferredAudioLanguage; + public final @Nullable String preferredAudioLanguage; // Text /** * The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the - * default track if there is one, or no track otherwise. + * default track if there is one, or no track otherwise. The default value is {@code null}. */ - public final String preferredTextLanguage; + public final @Nullable String preferredTextLanguage; /** - * Whether a text track with undetermined language should be selected if no track with - * {@link #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. + * Whether a text track with undetermined language should be selected if no track with {@link + * #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. The + * default value is {@code false}. */ public final boolean selectUndeterminedTextLanguage; /** * Bitmask of selection flags that are disabled for text track selections. See {@link - * C.SelectionFlags}. + * C.SelectionFlags}. The default value is {@code 0} (i.e. no flags). */ public final int disabledTextTrackSelectionFlags; // Video /** - * Maximum allowed video width. + * Maximum allowed video width. The default value is {@link Integer#MAX_VALUE} (i.e. no + * constraint). + * + *

    To constrain adaptive video track selections to be suitable for a given viewport (the + * region of the display within which video will be played), use ({@link #viewportWidth}, {@link + * #viewportHeight} and {@link #viewportOrientationMayChange}) instead. */ public final int maxVideoWidth; /** - * Maximum allowed video height. + * Maximum allowed video height. The default value is {@link Integer#MAX_VALUE} (i.e. no + * constraint). + * + *

    To constrain adaptive video track selections to be suitable for a given viewport (the + * region of the display within which video will be played), use ({@link #viewportWidth}, {@link + * #viewportHeight} and {@link #viewportOrientationMayChange}) instead. */ public final int maxVideoHeight; /** - * Maximum video bitrate. + * Maximum video bitrate. The default value is {@link Integer#MAX_VALUE} (i.e. no constraint). */ public final int maxVideoBitrate; /** - * Whether to exceed video constraints when no selection can be made otherwise. + * Whether to exceed the {@link #maxVideoWidth}, {@link #maxVideoHeight} and {@link + * #maxVideoBitrate} constraints when no selection can be made otherwise. The default value is + * {@code true}. */ public final boolean exceedVideoConstraintsIfNecessary; /** - * Viewport width in pixels. Constrains video tracks selections for adaptive playbacks so that - * only tracks suitable for the viewport are selected. + * Viewport width in pixels. Constrains video track selections for adaptive content so that only + * tracks suitable for the viewport are selected. The default value is {@link Integer#MAX_VALUE} + * (i.e. no constraint). */ public final int viewportWidth; /** - * Viewport height in pixels. Constrains video tracks selections for adaptive playbacks so that - * only tracks suitable for the viewport are selected. + * Viewport height in pixels. Constrains video track selections for adaptive content so that + * only tracks suitable for the viewport are selected. The default value is {@link + * Integer#MAX_VALUE} (i.e. no constraint). */ public final int viewportHeight; /** - * Whether the viewport orientation may change during playback. Constrains video tracks - * selections for adaptive playbacks so that only tracks suitable for the viewport are selected. + * Whether the viewport orientation may change during playback. Constrains video track + * selections for adaptive content so that only tracks suitable for the viewport are selected. + * The default value is {@code true}. */ public final boolean viewportOrientationMayChange; // General /** * Whether to force selection of the single lowest bitrate audio and video tracks that comply - * with all other constraints. + * with all other constraints. The default value is {@code false}. */ public final boolean forceLowestBitrate; /** - * Whether to allow adaptive selections containing mixed mime types. + * Whether to allow adaptive selections containing mixed mime types. The default value is {@code + * false}. */ public final boolean allowMixedMimeAdaptiveness; /** - * Whether to allow adaptive selections where adaptation may not be completely seamless. + * Whether to allow adaptive selections where adaptation may not be completely seamless. The + * default value is {@code true}. */ public final boolean allowNonSeamlessAdaptiveness; /** * Whether to exceed renderer capabilities when no selection can be made otherwise. + * + *

    This parameter applies when all of the tracks available for a renderer exceed the + * renderer's reported capabilities. If the parameter is {@code true} then the lowest quality + * track will still be selected. Playback may succeed if the renderer has under-reported its + * true capabilities. If {@code false} then no track will be selected. The default value is + * {@code true}. */ public final boolean exceedRendererCapabilitiesIfNecessary; /** * The audio session id to use when tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling - * is not to be enabled. + * is disabled. The default value is {@link C#AUDIO_SESSION_ID_UNSET} (i.e. tunneling is + * disabled). */ public final int tunnelingAudioSessionId; private Parameters() { this( - new SparseArray>(), - new SparseBooleanArray(), - null, - null, - false, - 0, - false, - false, - true, - Integer.MAX_VALUE, - Integer.MAX_VALUE, - Integer.MAX_VALUE, - true, - true, - Integer.MAX_VALUE, - Integer.MAX_VALUE, - true, - C.AUDIO_SESSION_ID_UNSET); + /* selectionOverrides= */ new SparseArray<>(), + /* rendererDisabledFlags= */ new SparseBooleanArray(), + /* preferredAudioLanguage= */ null, + /* preferredTextLanguage= */ null, + /* selectUndeterminedTextLanguage= */ false, + /* disabledTextTrackSelectionFlags= */ 0, + /* forceLowestBitrate= */ false, + /* allowMixedMimeAdaptiveness= */ false, + /* allowNonSeamlessAdaptiveness= */ true, + /* maxVideoWidth= */ Integer.MAX_VALUE, + /* maxVideoHeight= */ Integer.MAX_VALUE, + /* maxVideoBitrate= */ Integer.MAX_VALUE, + /* exceedVideoConstraintsIfNecessary= */ true, + /* exceedRendererCapabilitiesIfNecessary= */ true, + /* viewportWidth= */ Integer.MAX_VALUE, + /* viewportHeight= */ Integer.MAX_VALUE, + /* viewportOrientationMayChange= */ true, + /* tunnelingAudioSessionId= */ C.AUDIO_SESSION_ID_UNSET); } /* package */ Parameters( SparseArray> selectionOverrides, SparseBooleanArray rendererDisabledFlags, - String preferredAudioLanguage, - String preferredTextLanguage, + @Nullable String preferredAudioLanguage, + @Nullable String preferredTextLanguage, boolean selectUndeterminedTextLanguage, int disabledTextTrackSelectionFlags, boolean forceLowestBitrate, @@ -759,7 +771,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param groups The {@link TrackGroupArray}. * @return The override, or null if no override exists. */ - public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { + public final @Nullable SelectionOverride getSelectionOverride( + int rendererIndex, TrackGroupArray groups) { Map overrides = selectionOverrides.get(rendererIndex); return overrides != null ? overrides.get(groups) : null; } @@ -816,8 +829,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { result = 31 * result + viewportHeight; result = 31 * result + maxVideoBitrate; result = 31 * result + tunnelingAudioSessionId; - result = 31 * result + preferredAudioLanguage.hashCode(); - result = 31 * result + preferredTextLanguage.hashCode(); + result = + 31 * result + (preferredAudioLanguage == null ? 0 : preferredAudioLanguage.hashCode()); + result = 31 * result + (preferredTextLanguage == null ? 0 : preferredTextLanguage.hashCode()); return result; } @@ -1045,20 +1059,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final TrackSelection.Factory adaptiveTrackSelectionFactory; private final AtomicReference parametersReference; - /** - * Constructs an instance that does not support adaptive track selection. - */ + /** Constructs an instance that uses a default factory to create adaptive track selections. */ public DefaultTrackSelector() { - this((TrackSelection.Factory) null); + this(new AdaptiveTrackSelection.Factory()); } /** - * Constructs an instance that supports adaptive track selection. Adaptive track selections use - * the provided {@link BandwidthMeter} to determine which individual track should be used during - * playback. - * - * @param bandwidthMeter The {@link BandwidthMeter}. + * @deprecated Use {@link #DefaultTrackSelector()} instead. Custom bandwidth meter should be + * directly passed to the player in ExoPlayerFactory. */ + @Deprecated public DefaultTrackSelector(BandwidthMeter bandwidthMeter) { this(new AdaptiveTrackSelection.Factory(bandwidthMeter)); } @@ -1066,8 +1076,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * Constructs an instance that uses a factory to create adaptive track selections. * - * @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null if - * the selector should not support adaptive tracks. + * @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s. */ public DefaultTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) { this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory; @@ -1139,7 +1148,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** @deprecated Use {@link Parameters#getSelectionOverride(int, TrackGroupArray)}. */ @Deprecated - public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { + public final @Nullable SelectionOverride getSelectionOverride( + int rendererIndex, TrackGroupArray groups) { return getParameters().getSelectionOverride(rendererIndex, groups); } @@ -1170,14 +1180,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { // MappingTrackSelector implementation. @Override - protected final Pair selectTracks( - MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupports) - throws ExoPlaybackException { + protected final Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> + selectTracks( + MappedTrackInfo mappedTrackInfo, + int[][][] rendererFormatSupports, + int[] rendererMixedMimeTypeAdaptationSupports) + throws ExoPlaybackException { Parameters params = parametersReference.get(); int rendererCount = mappedTrackInfo.getRendererCount(); - TrackSelection[] rendererTrackSelections = + @NullableType TrackSelection[] rendererTrackSelections = selectAllTracks( mappedTrackInfo, rendererFormatSupports, @@ -1200,8 +1211,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { rendererTrackGroups.get(override.groupIndex), override.tracks[0]); } else { rendererTrackSelections[i] = - adaptiveTrackSelectionFactory.createTrackSelection( - rendererTrackGroups.get(override.groupIndex), override.tracks); + Assertions.checkNotNull(adaptiveTrackSelectionFactory) + .createTrackSelection( + rendererTrackGroups.get(override.groupIndex), + getBandwidthMeter(), + override.tracks); } } } @@ -1209,7 +1223,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Initialize the renderer configurations to the default configuration for all renderers with // selections, and null otherwise. - RendererConfiguration[] rendererConfigurations = new RendererConfiguration[rendererCount]; + @NullableType RendererConfiguration[] rendererConfigurations = + new RendererConfiguration[rendererCount]; for (int i = 0; i < rendererCount; i++) { boolean forceRendererDisabled = params.getRendererDisabled(i); boolean rendererEnabled = @@ -1248,14 +1263,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { * disabled, unless RendererCapabilities#getTrackType()} is {@link C#TRACK_TYPE_NONE}. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected TrackSelection[] selectAllTracks( + protected @NullableType TrackSelection[] selectAllTracks( MappedTrackInfo mappedTrackInfo, int[][][] rendererFormatSupports, int[] rendererMixedMimeTypeAdaptationSupports, Parameters params) throws ExoPlaybackException { int rendererCount = mappedTrackInfo.getRendererCount(); - TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCount]; + @NullableType TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCount]; boolean seenVideoRendererWithMappedTracks = false; boolean selectedVideoTracks = false; @@ -1331,12 +1346,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return The {@link TrackSelection} for the renderer, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected TrackSelection selectVideoTrack( + protected @Nullable TrackSelection selectVideoTrack( TrackGroupArray groups, int[][] formatSupports, int mixedMimeTypeAdaptationSupports, Parameters params, - TrackSelection.Factory adaptiveTrackSelectionFactory) + @Nullable TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { TrackSelection selection = null; if (!params.forceLowestBitrate && adaptiveTrackSelectionFactory != null) { @@ -1346,7 +1361,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { formatSupports, mixedMimeTypeAdaptationSupports, params, - adaptiveTrackSelectionFactory); + adaptiveTrackSelectionFactory, + getBandwidthMeter()); } if (selection == null) { selection = selectFixedVideoTrack(groups, formatSupports, params); @@ -1354,12 +1370,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { return selection; } - private static TrackSelection selectAdaptiveVideoTrack( + private static @Nullable TrackSelection selectAdaptiveVideoTrack( TrackGroupArray groups, int[][] formatSupport, int mixedMimeTypeAdaptationSupports, Parameters params, - TrackSelection.Factory adaptiveTrackSelectionFactory) + TrackSelection.Factory adaptiveTrackSelectionFactory, + BandwidthMeter bandwidthMeter) throws ExoPlaybackException { int requiredAdaptiveSupport = params.allowNonSeamlessAdaptiveness ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS) @@ -1374,7 +1391,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { params.maxVideoBitrate, params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange); if (adaptiveTracks.length > 0) { - return adaptiveTrackSelectionFactory.createTrackSelection(group, adaptiveTracks); + return Assertions.checkNotNull(adaptiveTrackSelectionFactory) + .createTrackSelection(group, bandwidthMeter, adaptiveTracks); } } return null; @@ -1397,7 +1415,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { String selectedMimeType = null; if (!allowMixedMimeTypes) { // Select the mime type for which we have the most adaptive tracks. - HashSet seenMimeTypes = new HashSet<>(); + HashSet<@NullableType String> seenMimeTypes = new HashSet<>(); int selectedMimeTypeTrackCount = 0; for (int i = 0; i < selectedTrackIndices.size(); i++) { int trackIndex = selectedTrackIndices.get(i); @@ -1421,9 +1439,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { return selectedTrackIndices.size() < 2 ? NO_TRACKS : Util.toArray(selectedTrackIndices); } - private static int getAdaptiveVideoTrackCountForMimeType(TrackGroup group, int[] formatSupport, - int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, int maxVideoHeight, - int maxVideoBitrate, List selectedTrackIndices) { + private static int getAdaptiveVideoTrackCountForMimeType( + TrackGroup group, + int[] formatSupport, + int requiredAdaptiveSupport, + @Nullable String mimeType, + int maxVideoWidth, + int maxVideoHeight, + int maxVideoBitrate, + List selectedTrackIndices) { int adaptiveTrackCount = 0; for (int i = 0; i < selectedTrackIndices.size(); i++) { int trackIndex = selectedTrackIndices.get(i); @@ -1436,9 +1460,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { return adaptiveTrackCount; } - private static void filterAdaptiveVideoTrackCountForMimeType(TrackGroup group, - int[] formatSupport, int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, - int maxVideoHeight, int maxVideoBitrate, List selectedTrackIndices) { + private static void filterAdaptiveVideoTrackCountForMimeType( + TrackGroup group, + int[] formatSupport, + int requiredAdaptiveSupport, + @Nullable String mimeType, + int maxVideoWidth, + int maxVideoHeight, + int maxVideoBitrate, + List selectedTrackIndices) { for (int i = selectedTrackIndices.size() - 1; i >= 0; i--) { int trackIndex = selectedTrackIndices.get(i); if (!isSupportedAdaptiveVideoTrack(group.getFormat(trackIndex), mimeType, @@ -1449,8 +1479,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } - private static boolean isSupportedAdaptiveVideoTrack(Format format, String mimeType, - int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight, + private static boolean isSupportedAdaptiveVideoTrack( + Format format, + @Nullable String mimeType, + int formatSupport, + int requiredAdaptiveSupport, + int maxVideoWidth, + int maxVideoHeight, int maxVideoBitrate) { return isSupported(formatSupport, false) && ((formatSupport & requiredAdaptiveSupport) != 0) && (mimeType == null || Util.areEqual(format.sampleMimeType, mimeType)) @@ -1459,7 +1494,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate); } - private static TrackSelection selectFixedVideoTrack( + private static @Nullable TrackSelection selectFixedVideoTrack( TrackGroupArray groups, int[][] formatSupports, Parameters params) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; @@ -1537,12 +1572,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return The {@link TrackSelection} for the renderer, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected TrackSelection selectAudioTrack( + protected @Nullable TrackSelection selectAudioTrack( TrackGroupArray groups, int[][] formatSupports, int mixedMimeTypeAdaptationSupports, Parameters params, - TrackSelection.Factory adaptiveTrackSelectionFactory) + @Nullable TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { int selectedTrackIndex = C.INDEX_UNSET; int selectedGroupIndex = C.INDEX_UNSET; @@ -1576,8 +1611,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { getAdaptiveAudioTracks( selectedGroup, formatSupports[selectedGroupIndex], params.allowMixedMimeAdaptiveness); if (adaptiveTracks.length > 0) { - return adaptiveTrackSelectionFactory.createTrackSelection(selectedGroup, - adaptiveTracks); + return adaptiveTrackSelectionFactory + .createTrackSelection(selectedGroup, getBandwidthMeter(), adaptiveTracks); } } return new FixedTrackSelection(selectedGroup, selectedTrackIndex); @@ -1606,8 +1641,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { int[] adaptiveIndices = new int[selectedConfigurationTrackCount]; int index = 0; for (int i = 0; i < group.length; i++) { - if (isSupportedAdaptiveAudioTrack(group.getFormat(i), formatSupport[i], - selectedConfiguration)) { + if (isSupportedAdaptiveAudioTrack( + group.getFormat(i), formatSupport[i], Assertions.checkNotNull(selectedConfiguration))) { adaptiveIndices[index++] = i; } } @@ -1648,7 +1683,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return The {@link TrackSelection} for the renderer, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected TrackSelection selectTextTrack( + protected @Nullable TrackSelection selectTextTrack( TrackGroupArray groups, int[][] formatSupport, Parameters params) throws ExoPlaybackException { TrackGroup selectedGroup = null; @@ -1721,7 +1756,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return The {@link TrackSelection} for the renderer, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected TrackSelection selectOtherTrack( + protected @Nullable TrackSelection selectOtherTrack( int trackType, TrackGroupArray groups, int[][] formatSupport, Parameters params) throws ExoPlaybackException { TrackGroup selectedGroup = null; @@ -1768,8 +1803,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static void maybeConfigureRenderersForTunneling( MappedTrackInfo mappedTrackInfo, int[][][] renderererFormatSupports, - RendererConfiguration[] rendererConfigurations, - TrackSelection[] trackSelections, + @NullableType RendererConfiguration[] rendererConfigurations, + @NullableType TrackSelection[] trackSelections, int tunnelingAudioSessionId) { if (tunnelingAudioSessionId == C.AUDIO_SESSION_ID_UNSET) { return; @@ -1883,15 +1918,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Returns whether a {@link Format} specifies a particular language, or {@code false} if - * {@code language} is null. + * Returns whether a {@link Format} specifies a particular language, or {@code false} if {@code + * language} is null. * * @param format The {@link Format}. * @param language The language. * @return Whether the format specifies the language, or {@code false} if {@code language} is * null. */ - protected static boolean formatHasLanguage(Format format, String language) { + protected static boolean formatHasLanguage(Format format, @Nullable String language) { return language != null && TextUtils.equals(language, Util.normalizeLanguageCode(format.language)); } @@ -1997,7 +2032,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * negative integer if this score is worse than the other. */ @Override - public int compareTo(@NonNull AudioTrackScore other) { + public int compareTo(AudioTrackScore other) { if (this.withinRendererCapabilitiesScore != other.withinRendererCapabilitiesScore) { return compareInts(this.withinRendererCapabilitiesScore, other.withinRendererCapabilitiesScore); @@ -2066,9 +2101,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { public final int channelCount; public final int sampleRate; - public final String mimeType; + public final @Nullable String mimeType; - public AudioConfigurationTuple(int channelCount, int sampleRate, String mimeType) { + public AudioConfigurationTuple(int channelCount, int sampleRate, @Nullable String mimeType) { this.channelCount = channelCount; this.sampleRate = sampleRate; this.mimeType = mimeType; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java index 50873d372d..b41698521f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2.trackselection; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.Assertions; /** @@ -30,7 +32,7 @@ public final class FixedTrackSelection extends BaseTrackSelection { public static final class Factory implements TrackSelection.Factory { private final int reason; - private final Object data; + private final @Nullable Object data; public Factory() { this.reason = C.SELECTION_REASON_UNKNOWN; @@ -41,21 +43,21 @@ public final class FixedTrackSelection extends BaseTrackSelection { * @param reason A reason for the track selection. * @param data Optional data associated with the track selection. */ - public Factory(int reason, Object data) { + public Factory(int reason, @Nullable Object data) { this.reason = reason; this.data = data; } @Override - public FixedTrackSelection createTrackSelection(TrackGroup group, int... tracks) { + public FixedTrackSelection createTrackSelection( + TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) { Assertions.checkArgument(tracks.length == 1); return new FixedTrackSelection(group, tracks[0], reason, data); } - } private final int reason; - private final Object data; + private final @Nullable Object data; /** * @param group The {@link TrackGroup}. Must not be null. @@ -71,7 +73,7 @@ public final class FixedTrackSelection extends BaseTrackSelection { * @param reason A reason for the track selection. * @param data Optional data associated with the track selection. */ - public FixedTrackSelection(TrackGroup group, int track, int reason, Object data) { + public FixedTrackSelection(TrackGroup group, int track, int reason, @Nullable Object data) { super(group, track); this.reason = reason; this.data = data; @@ -94,7 +96,7 @@ public final class FixedTrackSelection extends BaseTrackSelection { } @Override - public Object getSelectionData() { + public @Nullable Object getSelectionData() { return data; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 4af969369e..99e4e58c4a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.trackselection; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -28,6 +29,7 @@ import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Base class for {@link TrackSelector}s that first establish a mapping between {@link TrackGroup}s @@ -301,13 +303,13 @@ public abstract class MappingTrackSelector extends TrackSelector { } - private MappedTrackInfo currentMappedTrackInfo; + private @Nullable MappedTrackInfo currentMappedTrackInfo; /** * Returns the mapping information for the currently active track selection, or null if no * selection is currently active. */ - public final MappedTrackInfo getCurrentMappedTrackInfo() { + public final @Nullable MappedTrackInfo getCurrentMappedTrackInfo() { return currentMappedTrackInfo; } @@ -357,9 +359,11 @@ public abstract class MappingTrackSelector extends TrackSelector { int[] rendererTrackTypes = new int[rendererCapabilities.length]; for (int i = 0; i < rendererCapabilities.length; i++) { int rendererTrackGroupCount = rendererTrackGroupCounts[i]; - rendererTrackGroupArrays[i] = new TrackGroupArray( - Arrays.copyOf(rendererTrackGroups[i], rendererTrackGroupCount)); - rendererFormatSupports[i] = Arrays.copyOf(rendererFormatSupports[i], rendererTrackGroupCount); + rendererTrackGroupArrays[i] = + new TrackGroupArray( + Util.nullSafeArrayCopy(rendererTrackGroups[i], rendererTrackGroupCount)); + rendererFormatSupports[i] = + Util.nullSafeArrayCopy(rendererFormatSupports[i], rendererTrackGroupCount); rendererTrackTypes[i] = rendererCapabilities[i].getTrackType(); } @@ -367,7 +371,7 @@ public abstract class MappingTrackSelector extends TrackSelector { int unmappedTrackGroupCount = rendererTrackGroupCounts[rendererCapabilities.length]; TrackGroupArray unmappedTrackGroupArray = new TrackGroupArray( - Arrays.copyOf( + Util.nullSafeArrayCopy( rendererTrackGroups[rendererCapabilities.length], unmappedTrackGroupCount)); // Package up the track information and selections. @@ -379,7 +383,7 @@ public abstract class MappingTrackSelector extends TrackSelector { rendererFormatSupports, unmappedTrackGroupArray); - Pair result = + Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> result = selectTracks( mappedTrackInfo, rendererFormatSupports, rendererMixedMimeTypeAdaptationSupports); return new TrackSelectorResult(result.first, result.second, mappedTrackInfo); @@ -399,11 +403,12 @@ public abstract class MappingTrackSelector extends TrackSelector { * RendererCapabilities#getTrackType()} is {@link C#TRACK_TYPE_NONE}. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected abstract Pair selectTracks( - MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupport) - throws ExoPlaybackException; + protected abstract Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> + selectTracks( + MappedTrackInfo mappedTrackInfo, + int[][][] rendererFormatSupports, + int[] rendererMixedMimeTypeAdaptationSupport) + throws ExoPlaybackException; /** * Finds the renderer to which the provided {@link TrackGroup} should be mapped. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java index d11344a6f6..2ea90761af 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java @@ -16,8 +16,10 @@ package com.google.android.exoplayer2.trackselection; import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.upstream.BandwidthMeter; import java.util.Random; /** @@ -44,10 +46,10 @@ public final class RandomTrackSelection extends BaseTrackSelection { } @Override - public RandomTrackSelection createTrackSelection(TrackGroup group, int... tracks) { + public RandomTrackSelection createTrackSelection( + TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) { return new RandomTrackSelection(group, tracks, random); } - } private final Random random; @@ -123,7 +125,7 @@ public final class RandomTrackSelection extends BaseTrackSelection { } @Override - public Object getSelectionData() { + public @Nullable Object getSelectionData() { return null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index 55e6050622..0650159cf4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -15,10 +15,12 @@ */ package com.google.android.exoplayer2.trackselection; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.upstream.BandwidthMeter; import java.util.List; /** @@ -39,12 +41,13 @@ public interface TrackSelection { * Creates a new selection. * * @param group The {@link TrackGroup}. Must not be null. + * @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks. * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be * null or empty. May be in any order. * @return The created selection. */ - TrackSelection createTrackSelection(TrackGroup group, int... tracks); - + TrackSelection createTrackSelection( + TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks); } /** @@ -90,7 +93,9 @@ public interface TrackSelection { int getIndexInTrackGroup(int index); /** - * Returns the index in the selection of the track with the specified format. + * Returns the index in the selection of the track with the specified format. The format is + * located by identity so, for example, {@code selection.indexOf(selection.getFormat(index)) == + * index} even if multiple selected tracks have formats that contain the same values. * * @param format The format. * @return The index in the selection, or {@link C#INDEX_UNSET} if the track with the specified @@ -129,10 +134,8 @@ public interface TrackSelection { */ int getSelectionReason(); - /** - * Returns optional data associated with the current track selection. - */ - Object getSelectionData(); + /** Returns optional data associated with the current track selection. */ + @Nullable Object getSelectionData(); // Adaptation. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java index b37c8cad67..48151002be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.trackselection; import android.support.annotation.Nullable; import java.util.Arrays; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** An array of {@link TrackSelection}s. */ public final class TrackSelectionArray { @@ -24,15 +25,13 @@ public final class TrackSelectionArray { /** The length of this array. */ public final int length; - private final TrackSelection[] trackSelections; + private final @NullableType TrackSelection[] trackSelections; // Lazily initialized hashcode. private int hashCode; - /** - * @param trackSelections The selections. Must not be null, but may contain null elements. - */ - public TrackSelectionArray(TrackSelection... trackSelections) { + /** @param trackSelections The selections. Must not be null, but may contain null elements. */ + public TrackSelectionArray(@NullableType TrackSelection... trackSelections) { this.trackSelections = trackSelections; this.length = trackSelections.length; } @@ -43,14 +42,12 @@ public final class TrackSelectionArray { * @param index The index of the selection. * @return The selection. */ - public TrackSelection get(int index) { + public @Nullable TrackSelection get(int index) { return trackSelections[index]; } - /** - * Returns the selections in a newly allocated array. - */ - public TrackSelection[] getAll() { + /** Returns the selections in a newly allocated array. */ + public @NullableType TrackSelection[] getAll() { return trackSelections.clone(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java index a26fee6f78..3bb603318f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java @@ -15,12 +15,15 @@ */ package com.google.android.exoplayer2.trackselection; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.util.Assertions; /** * The component of an {@link ExoPlayer} responsible for selecting tracks to be consumed by each of @@ -28,48 +31,52 @@ import com.google.android.exoplayer2.source.TrackGroupArray; * suitable for most use cases. * *

    Interactions with the player

    + * * The following interactions occur between the player and its track selector during playback. + * *

    + * *

      - *
    • When the player is created it will initialize the track selector by calling - * {@link #init(InvalidationListener)}.
    • - *
    • When the player needs to make a track selection it will call - * {@link #selectTracks(RendererCapabilities[], TrackGroupArray)}. This typically occurs at the - * start of playback, when the player starts to buffer a new period of the media being played, - * and when the track selector invalidates its previous selections.
    • - *
    • The player may perform a track selection well in advance of the selected tracks becoming - * active, where active is defined to mean that the renderers are actually consuming media - * corresponding to the selection that was made. For example when playing media containing - * multiple periods, the track selection for a period is made when the player starts to buffer - * that period. Hence if the player's buffering policy is to maintain a 30 second buffer, the - * selection will occur approximately 30 seconds in advance of it becoming active. In fact the - * selection may never become active, for example if the user seeks to some other period of the - * media during the 30 second gap. The player indicates to the track selector when a selection - * it has previously made becomes active by calling {@link #onSelectionActivated(Object)}.
    • - *
    • If the track selector wishes to indicate to the player that selections it has previously - * made are invalid, it can do so by calling - * {@link InvalidationListener#onTrackSelectionsInvalidated()} on the - * {@link InvalidationListener} that was passed to {@link #init(InvalidationListener)}. A - * track selector may wish to do this if its configuration has changed, for example if it now - * wishes to prefer audio tracks in a particular language. This will trigger the player to make - * new track selections. Note that the player will have to re-buffer in the case that the new - * track selection for the currently playing period differs from the one that was invalidated. - *
    • + *
    • When the player is created it will initialize the track selector by calling {@link + * #init(InvalidationListener, BandwidthMeter)}. + *
    • When the player needs to make a track selection it will call {@link + * #selectTracks(RendererCapabilities[], TrackGroupArray)}. This typically occurs at the start + * of playback, when the player starts to buffer a new period of the media being played, and + * when the track selector invalidates its previous selections. + *
    • The player may perform a track selection well in advance of the selected tracks becoming + * active, where active is defined to mean that the renderers are actually consuming media + * corresponding to the selection that was made. For example when playing media containing + * multiple periods, the track selection for a period is made when the player starts to buffer + * that period. Hence if the player's buffering policy is to maintain a 30 second buffer, the + * selection will occur approximately 30 seconds in advance of it becoming active. In fact the + * selection may never become active, for example if the user seeks to some other period of + * the media during the 30 second gap. The player indicates to the track selector when a + * selection it has previously made becomes active by calling {@link + * #onSelectionActivated(Object)}. + *
    • If the track selector wishes to indicate to the player that selections it has previously + * made are invalid, it can do so by calling {@link + * InvalidationListener#onTrackSelectionsInvalidated()} on the {@link InvalidationListener} + * that was passed to {@link #init(InvalidationListener, BandwidthMeter)}. A track selector + * may wish to do this if its configuration has changed, for example if it now wishes to + * prefer audio tracks in a particular language. This will trigger the player to make new + * track selections. Note that the player will have to re-buffer in the case that the new + * track selection for the currently playing period differs from the one that was invalidated. *
    * *

    Renderer configuration

    - * The {@link TrackSelectorResult} returned by - * {@link #selectTracks(RendererCapabilities[], TrackGroupArray)} contains not only - * {@link TrackSelection}s for each renderer, but also {@link RendererConfiguration}s defining - * configuration parameters that the renderers should apply when consuming the corresponding media. - * Whilst it may seem counter-intuitive for a track selector to also specify renderer configuration - * information, in practice the two are tightly bound together. It may only be possible to play a - * certain combination tracks if the renderers are configured in a particular way. Equally, it may - * only be possible to configure renderers in a particular way if certain tracks are selected. Hence - * it makes sense to determined the track selection and corresponding renderer configurations in a - * single step. + * + * The {@link TrackSelectorResult} returned by {@link #selectTracks(RendererCapabilities[], + * TrackGroupArray)} contains not only {@link TrackSelection}s for each renderer, but also {@link + * RendererConfiguration}s defining configuration parameters that the renderers should apply when + * consuming the corresponding media. Whilst it may seem counter-intuitive for a track selector to + * also specify renderer configuration information, in practice the two are tightly bound together. + * It may only be possible to play a certain combination tracks if the renderers are configured in a + * particular way. Equally, it may only be possible to configure renderers in a particular way if + * certain tracks are selected. Hence it makes sense to determined the track selection and + * corresponding renderer configurations in a single step. * *

    Threading model

    + * * All calls made by the player into the track selector are on the player's internal playback * thread. The track selector may call {@link InvalidationListener#onTrackSelectionsInvalidated()} * from any thread. @@ -89,16 +96,19 @@ public abstract class TrackSelector { } - private InvalidationListener listener; + private @Nullable InvalidationListener listener; + private @Nullable BandwidthMeter bandwidthMeter; /** * Called by the player to initialize the selector. * * @param listener An invalidation listener that the selector can call to indicate that selections * it has previously made are no longer valid. + * @param bandwidthMeter A bandwidth meter which can be used by track selections to select tracks. */ - public final void init(InvalidationListener listener) { + public final void init(InvalidationListener listener, BandwidthMeter bandwidthMeter) { this.listener = listener; + this.bandwidthMeter = bandwidthMeter; } /** @@ -131,4 +141,11 @@ public abstract class TrackSelector { } } + /** + * Returns a bandwidth meter which can be used by track selections to select tracks. Must only be + * called after {@link #init(InvalidationListener, BandwidthMeter)} has been called. + */ + protected final BandwidthMeter getBandwidthMeter() { + return Assertions.checkNotNull(bandwidthMeter); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java index 882d98764e..f1136f0be5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.trackselection; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.util.Util; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * The result of a {@link TrackSelector} operation. @@ -29,7 +30,7 @@ public final class TrackSelectorResult { * A {@link RendererConfiguration} for each renderer. A null entry indicates the corresponding * renderer should be disabled. */ - public final RendererConfiguration[] rendererConfigurations; + public final @NullableType RendererConfiguration[] rendererConfigurations; /** * A {@link TrackSelectionArray} containing the track selection for each renderer. */ @@ -48,7 +49,9 @@ public final class TrackSelectorResult { * TrackSelector#onSelectionActivated(Object)} should the selection be activated. */ public TrackSelectorResult( - RendererConfiguration[] rendererConfigurations, TrackSelection[] selections, Object info) { + @NullableType RendererConfiguration[] rendererConfigurations, + @NullableType TrackSelection[] selections, + Object info) { this.rendererConfigurations = rendererConfigurations; this.selections = new TrackSelectionArray(selections); this.info = info; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java index d0b18bb765..16c27ccde8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java @@ -18,15 +18,14 @@ package com.google.android.exoplayer2.upstream; import android.content.Context; import android.content.res.AssetManager; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; -/** - * A {@link DataSource} for reading from a local asset. - */ -public final class AssetDataSource implements DataSource { +/** A {@link DataSource} for reading from a local asset. */ +public final class AssetDataSource extends BaseDataSource { /** * Thrown when an {@link IOException} is encountered reading a local asset. @@ -40,27 +39,30 @@ public final class AssetDataSource implements DataSource { } private final AssetManager assetManager; - private final TransferListener listener; - private Uri uri; - private InputStream inputStream; + private @Nullable Uri uri; + private @Nullable InputStream inputStream; private long bytesRemaining; private boolean opened; - /** - * @param context A context. - */ + /** @param context A context. */ public AssetDataSource(Context context) { - this(context, null); + super(/* isNetwork= */ false); + this.assetManager = context.getAssets(); } /** * @param context A context. * @param listener An optional listener. + * @deprecated Use {@link #AssetDataSource(Context)} and {@link + * #addTransferListener(TransferListener)}. */ - public AssetDataSource(Context context, TransferListener listener) { - this.assetManager = context.getAssets(); - this.listener = listener; + @Deprecated + public AssetDataSource(Context context, @Nullable TransferListener listener) { + this(context); + if (listener != null) { + addTransferListener(listener); + } } @Override @@ -73,6 +75,7 @@ public final class AssetDataSource implements DataSource { } else if (path.startsWith("/")) { path = path.substring(1); } + transferInitializing(dataSpec); inputStream = assetManager.open(path, AssetManager.ACCESS_RANDOM); long skipped = inputStream.skip(dataSpec.position); if (skipped < dataSpec.position) { @@ -96,9 +99,7 @@ public final class AssetDataSource implements DataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesRemaining; } @@ -129,14 +130,12 @@ public final class AssetDataSource implements DataSource { if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= bytesRead; } - if (listener != null) { - listener.onBytesTransferred(this, bytesRead); - } + bytesTransferred(bytesRead); return bytesRead; } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return uri; } @@ -153,9 +152,7 @@ public final class AssetDataSource implements DataSource { inputStream = null; if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java index 0a3fb967a8..470937f02f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.upstream; +import android.os.Handler; +import android.support.annotation.Nullable; + /** * Provides estimates of the currently available bandwidth. */ @@ -40,4 +43,26 @@ public interface BandwidthMeter { /** Returns the estimated bandwidth in bits/sec. */ long getBitrateEstimate(); + + /** + * Returns the {@link TransferListener} that this instance uses to gather bandwidth information + * from data transfers. May be null, if no transfer listener is used. + */ + @Nullable + TransferListener getTransferListener(); + + /** + * Adds an {@link EventListener} to be informed of bandwidth samples. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + */ + void addEventListener(Handler eventHandler, EventListener eventListener); + + /** + * Removes an {@link EventListener}. + * + * @param eventListener The listener to be removed. + */ + void removeEventListener(EventListener eventListener); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java new file mode 100644 index 0000000000..18a7dcea49 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.upstream; + +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import android.support.annotation.Nullable; +import java.util.ArrayList; + +/** + * Base {@link DataSource} implementation to keep a list of {@link TransferListener}s. + * + *

    Subclasses must call {@link #transferInitializing(DataSpec)}, {@link + * #transferStarted(DataSpec)}, {@link #bytesTransferred(int)}, and {@link #transferEnded()} to + * inform listeners of data transfers. + */ +public abstract class BaseDataSource implements DataSource { + + private final boolean isNetwork; + private final ArrayList listeners; + + private int listenerCount; + private @Nullable DataSpec dataSpec; + + /** + * Creates base data source. + * + * @param isNetwork Whether the data source loads data through a network. + */ + protected BaseDataSource(boolean isNetwork) { + this.isNetwork = isNetwork; + this.listeners = new ArrayList<>(/* initialCapacity= */ 1); + } + + @Override + public final void addTransferListener(TransferListener transferListener) { + listeners.add(transferListener); + listenerCount++; + } + + /** + * Notifies listeners that data transfer for the specified {@link DataSpec} is being initialized. + * + * @param dataSpec {@link DataSpec} describing the data for initializing transfer. + */ + protected final void transferInitializing(DataSpec dataSpec) { + for (int i = 0; i < listenerCount; i++) { + listeners.get(i).onTransferInitializing(/* source= */ this, dataSpec, isNetwork); + } + } + + /** + * Notifies listeners that data transfer for the specified {@link DataSpec} started. + * + * @param dataSpec {@link DataSpec} describing the data being transferred. + */ + protected final void transferStarted(DataSpec dataSpec) { + this.dataSpec = dataSpec; + for (int i = 0; i < listenerCount; i++) { + listeners.get(i).onTransferStart(/* source= */ this, dataSpec, isNetwork); + } + } + + /** + * Notifies listeners that bytes were transferred. + * + * @param bytesTransferred The number of bytes transferred since the previous call to this method + * (or if the first call, since the transfer was started). + */ + protected final void bytesTransferred(int bytesTransferred) { + DataSpec dataSpec = castNonNull(this.dataSpec); + for (int i = 0; i < listenerCount; i++) { + listeners + .get(i) + .onBytesTransferred(/* source= */ this, dataSpec, isNetwork, bytesTransferred); + } + } + + /** Notifies listeners that a transfer ended. */ + protected final void transferEnded() { + DataSpec dataSpec = castNonNull(this.dataSpec); + for (int i = 0; i < listenerCount; i++) { + listeners.get(i).onTransferEnd(/* source= */ this, dataSpec, isNetwork); + } + this.dataSpec = null; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java index e5311e783b..16637b4052 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java @@ -16,25 +16,26 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; -/** - * A {@link DataSource} for reading from a byte array. - */ -public final class ByteArrayDataSource implements DataSource { +/** A {@link DataSource} for reading from a byte array. */ +public final class ByteArrayDataSource extends BaseDataSource { private final byte[] data; - private Uri uri; + private @Nullable Uri uri; private int readPosition; private int bytesRemaining; + private boolean opened; /** * @param data The data to be read. */ public ByteArrayDataSource(byte[] data) { + super(/* isNetwork= */ false); Assertions.checkNotNull(data); Assertions.checkArgument(data.length > 0); this.data = data; @@ -43,6 +44,7 @@ public final class ByteArrayDataSource implements DataSource { @Override public long open(DataSpec dataSpec) throws IOException { uri = dataSpec.uri; + transferInitializing(dataSpec); readPosition = (int) dataSpec.position; bytesRemaining = (int) ((dataSpec.length == C.LENGTH_UNSET) ? (data.length - dataSpec.position) : dataSpec.length); @@ -50,6 +52,8 @@ public final class ByteArrayDataSource implements DataSource { throw new IOException("Unsatisfiable range: [" + readPosition + ", " + dataSpec.length + "], length: " + data.length); } + opened = true; + transferStarted(dataSpec); return bytesRemaining; } @@ -65,16 +69,21 @@ public final class ByteArrayDataSource implements DataSource { System.arraycopy(data, readPosition, buffer, offset, readLength); readPosition += readLength; bytesRemaining -= readLength; + bytesTransferred(readLength); return readLength; } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return uri; } @Override public void close() throws IOException { + if (opened) { + opened = false; + transferEnded(); + } uri = null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index 87642e0eba..273509e0d4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -19,6 +19,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.io.EOFException; import java.io.FileInputStream; @@ -26,10 +27,8 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.nio.channels.FileChannel; -/** - * A {@link DataSource} for reading from a content URI. - */ -public final class ContentDataSource implements DataSource { +/** A {@link DataSource} for reading from a content URI. */ +public final class ContentDataSource extends BaseDataSource { /** * Thrown when an {@link IOException} is encountered reading from a content URI. @@ -43,11 +42,10 @@ public final class ContentDataSource implements DataSource { } private final ContentResolver resolver; - private final TransferListener listener; - private Uri uri; - private AssetFileDescriptor assetFileDescriptor; - private FileInputStream inputStream; + private @Nullable Uri uri; + private @Nullable AssetFileDescriptor assetFileDescriptor; + private @Nullable FileInputStream inputStream; private long bytesRemaining; private boolean opened; @@ -55,22 +53,29 @@ public final class ContentDataSource implements DataSource { * @param context A context. */ public ContentDataSource(Context context) { - this(context, null); + super(/* isNetwork= */ false); + this.resolver = context.getContentResolver(); } /** * @param context A context. * @param listener An optional listener. + * @deprecated Use {@link #ContentDataSource(Context)} and {@link + * #addTransferListener(TransferListener)}. */ - public ContentDataSource(Context context, TransferListener listener) { - this.resolver = context.getContentResolver(); - this.listener = listener; + @Deprecated + public ContentDataSource(Context context, @Nullable TransferListener listener) { + this(context); + if (listener != null) { + addTransferListener(listener); + } } @Override public long open(DataSpec dataSpec) throws ContentDataSourceException { try { uri = dataSpec.uri; + transferInitializing(dataSpec); assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r"); if (assetFileDescriptor == null) { throw new FileNotFoundException("Could not open file descriptor for: " + uri); @@ -102,9 +107,7 @@ public final class ContentDataSource implements DataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesRemaining; } @@ -136,14 +139,12 @@ public final class ContentDataSource implements DataSource { if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= bytesRead; } - if (listener != null) { - listener.onBytesTransferred(this, bytesRead); - } + bytesTransferred(bytesRead); return bytesRead; } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return uri; } @@ -168,9 +169,7 @@ public final class ContentDataSource implements DataSource { assetFileDescriptor = null; if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java index c547625819..5a2f5d153c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java @@ -16,33 +16,38 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.support.annotation.Nullable; import android.util.Base64; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.net.URLDecoder; -/** - * A {@link DataSource} for reading data URLs, as defined by RFC 2397. - */ -public final class DataSchemeDataSource implements DataSource { +/** A {@link DataSource} for reading data URLs, as defined by RFC 2397. */ +public final class DataSchemeDataSource extends BaseDataSource { public static final String SCHEME_DATA = "data"; - private DataSpec dataSpec; + private @Nullable DataSpec dataSpec; private int bytesRead; - private byte[] data; + private @Nullable byte[] data; + + public DataSchemeDataSource() { + super(/* isNetwork= */ false); + } @Override public long open(DataSpec dataSpec) throws IOException { + transferInitializing(dataSpec); this.dataSpec = dataSpec; Uri uri = dataSpec.uri; String scheme = uri.getScheme(); if (!SCHEME_DATA.equals(scheme)) { throw new ParserException("Unsupported scheme: " + scheme); } - String[] uriParts = uri.getSchemeSpecificPart().split(","); - if (uriParts.length > 2) { + String[] uriParts = Util.split(uri.getSchemeSpecificPart(), ","); + if (uriParts.length != 2) { throw new ParserException("Unexpected URI format: " + uri); } String dataString = uriParts[1]; @@ -56,6 +61,7 @@ public final class DataSchemeDataSource implements DataSource { // TODO: Add support for other charsets. data = URLDecoder.decode(dataString, C.ASCII_NAME).getBytes(); } + transferStarted(dataSpec); return data.length; } @@ -71,18 +77,22 @@ public final class DataSchemeDataSource implements DataSource { readLength = Math.min(readLength, remainingBytes); System.arraycopy(data, bytesRead, buffer, offset, readLength); bytesRead += readLength; + bytesTransferred(readLength); return readLength; } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return dataSpec != null ? dataSpec.uri : null; } @Override public void close() throws IOException { + if (data != null) { + data = null; + transferEnded(); + } dataSpec = null; - data = null; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java index ce3230fa43..c759499577 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java @@ -19,6 +19,9 @@ import android.net.Uri; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; /** * A component from which streams of data can be read. @@ -34,9 +37,15 @@ public interface DataSource { * Creates a {@link DataSource} instance. */ DataSource createDataSource(); - } + /** + * Adds a {@link TransferListener} to listen to data transfers. This method is not thread-safe. + * + * @param transferListener A {@link TransferListener}. + */ + void addTransferListener(TransferListener transferListener); + /** * Opens the source to read the specified data. *

    @@ -82,6 +91,14 @@ public interface DataSource { */ @Nullable Uri getUri(); + /** + * When the source is open, returns the response headers associated with the last {@link #open} + * call. Otherwise, returns an empty map. + */ + default Map> getResponseHeaders() { + return Collections.emptyMap(); + } + /** * Closes the source. *

    @@ -91,5 +108,4 @@ public interface DataSource { * @throws IOException If an error occurs closing the source. */ void close() throws IOException; - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index ad7a9d0147..366b6d8c67 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -46,22 +46,41 @@ public final class DataSpec { * {@link DataSource#open(DataSpec)} will typically be {@link C#LENGTH_UNSET}. The data read from * {@link DataSource#read(byte[], int, int)} will be the decompressed data. */ - public static final int FLAG_ALLOW_GZIP = 1 << 0; + public static final int FLAG_ALLOW_GZIP = 1; /** * Permits content to be cached even if its length can not be resolved. Typically this's the case * for progressive live streams and when {@link #FLAG_ALLOW_GZIP} is used. */ - public static final int FLAG_ALLOW_CACHING_UNKNOWN_LENGTH = 1 << 1; + public static final int FLAG_ALLOW_CACHING_UNKNOWN_LENGTH = 1 << 1; // 2 + + /** The set of HTTP methods that are supported by ExoPlayer {@link HttpDataSource}s. */ + @IntDef({HTTP_METHOD_GET, HTTP_METHOD_POST, HTTP_METHOD_HEAD}) + public @interface HttpMethod {} + + public static final int HTTP_METHOD_GET = 1; + public static final int HTTP_METHOD_POST = 2; + public static final int HTTP_METHOD_HEAD = 3; /** * The source from which data should be read. */ public final Uri uri; + /** - * Body for a POST request, null otherwise. + * The HTTP method, which will be used by {@link HttpDataSource} when requesting this DataSpec. + * This value will be ignored by non-http {@link DataSource}s. */ - public final @Nullable byte[] postBody; + public final @HttpMethod int httpMethod; + + /** + * The HTTP body, null otherwise. If the body is non-null, then httpBody.length will be non-zero. + */ + public final @Nullable byte[] httpBody; + + /** @deprecated Use {@link #httpBody} instead. */ + @Deprecated public final @Nullable byte[] postBody; + /** * The absolute position of the data in the full stream. */ @@ -155,11 +174,13 @@ public final class DataSpec { } /** - * Construct a {@link DataSpec} where {@link #position} may differ from {@link - * #absoluteStreamPosition}. + * Construct a {@link DataSpec} by inferring the {@link #httpMethod} based on the {@code postBody} + * parameter. If postBody is non-null, then httpMethod is set to {@link #HTTP_METHOD_POST}. If + * postBody is null, then httpMethod is set to {@link #HTTP_METHOD_GET}. * * @param uri {@link #uri}. - * @param postBody {@link #postBody}. + * @param postBody {@link #httpBody} The body of the HTTP request, which is also used to infer the + * {@link #httpMethod}. * @param absoluteStreamPosition {@link #absoluteStreamPosition}. * @param position {@link #position}. * @param length {@link #length}. @@ -174,11 +195,46 @@ public final class DataSpec { long length, @Nullable String key, @Flags int flags) { + this( + uri, + /* httpMethod= */ postBody != null ? HTTP_METHOD_POST : HTTP_METHOD_GET, + /* httpBody= */ postBody, + absoluteStreamPosition, + position, + length, + key, + flags); + } + + /** + * Construct a {@link DataSpec} where {@link #position} may differ from {@link + * #absoluteStreamPosition}. + * + * @param uri {@link #uri}. + * @param httpMethod {@link #httpMethod}. + * @param httpBody {@link #httpBody}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}. + * @param position {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + */ + public DataSpec( + Uri uri, + @HttpMethod int httpMethod, + @Nullable byte[] httpBody, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, + @Flags int flags) { Assertions.checkArgument(absoluteStreamPosition >= 0); Assertions.checkArgument(position >= 0); Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET); this.uri = uri; - this.postBody = postBody; + this.httpMethod = httpMethod; + this.httpBody = (httpBody != null && httpBody.length != 0) ? httpBody : null; + this.postBody = this.httpBody; this.absoluteStreamPosition = absoluteStreamPosition; this.position = position; this.length = length; @@ -197,8 +253,48 @@ public final class DataSpec { @Override public String toString() { - return "DataSpec[" + uri + ", " + Arrays.toString(postBody) + ", " + absoluteStreamPosition - + ", " + position + ", " + length + ", " + key + ", " + flags + "]"; + return "DataSpec[" + + getHttpMethodString() + + " " + + uri + + ", " + + Arrays.toString(httpBody) + + ", " + + absoluteStreamPosition + + ", " + + position + + ", " + + length + + ", " + + key + + ", " + + flags + + "]"; + } + + /** + * Returns an uppercase HTTP method name (e.g., "GET", "POST", "HEAD") corresponding to the {@link + * #httpMethod}. + */ + public final String getHttpMethodString() { + return getStringForHttpMethod(httpMethod); + } + + /** + * Returns an uppercase HTTP method name (e.g., "GET", "POST", "HEAD") corresponding to the {@code + * httpMethod}. + */ + public static String getStringForHttpMethod(@HttpMethod int httpMethod) { + switch (httpMethod) { + case HTTP_METHOD_GET: + return "GET"; + case HTTP_METHOD_POST: + return "POST"; + case HTTP_METHOD_HEAD: + return "HEAD"; + default: + throw new AssertionError(httpMethod); + } } /** @@ -223,8 +319,15 @@ public final class DataSpec { if (offset == 0 && this.length == length) { return this; } else { - return new DataSpec(uri, postBody, absoluteStreamPosition + offset, position + offset, length, - key, flags); + return new DataSpec( + uri, + httpMethod, + httpBody, + absoluteStreamPosition + offset, + position + offset, + length, + key, + flags); } } @@ -235,6 +338,7 @@ public final class DataSpec { * @return The copied {@link DataSpec} with the specified Uri. */ public DataSpec withUri(Uri uri) { - return new DataSpec(uri, postBody, absoluteStreamPosition, position, length, key, flags); + return new DataSpec( + uri, httpMethod, httpBody, absoluteStreamPosition, position, length, key, flags); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java index d9bd5873f0..06ca83dd93 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java @@ -118,8 +118,18 @@ public final class DefaultAllocator implements Allocator { } for (Allocation allocation : allocations) { // Weak sanity check that the allocation probably originated from this pool. - Assertions.checkArgument(allocation.data == initialAllocationBlock - || allocation.data.length == individualAllocationSize); + if (allocation.data != initialAllocationBlock + && allocation.data.length != individualAllocationSize) { + throw new IllegalArgumentException( + "Unexpected allocation: " + + System.identityHashCode(allocation.data) + + ", " + + System.identityHashCode(initialAllocationBlock) + + ", " + + allocation.data.length + + ", " + + individualAllocationSize); + } availableAllocations[availableCount++] = allocation; } allocatedCount -= allocations.length; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java index f32965619a..6e0fba27ae 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java @@ -19,13 +19,14 @@ import android.os.Handler; import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.EventDispatcher; import com.google.android.exoplayer2.util.SlidingPercentile; /** - * Estimates bandwidth by listening to data transfers. The bandwidth estimate is calculated using - * a {@link SlidingPercentile} and is updated each time a transfer ends. + * Estimates bandwidth by listening to data transfers. The bandwidth estimate is calculated using a + * {@link SlidingPercentile} and is updated each time a transfer ends. */ -public final class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { +public final class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { /** Default initial bitrate estimate in bits per second. */ public static final long DEFAULT_INITIAL_BITRATE_ESTIMATE = 1_000_000; @@ -105,16 +106,19 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList * @return A bandwidth meter with the configured properties. */ public DefaultBandwidthMeter build() { - return new DefaultBandwidthMeter( - eventHandler, eventListener, initialBitrateEstimate, slidingWindowMaxWeight, clock); + DefaultBandwidthMeter bandwidthMeter = + new DefaultBandwidthMeter(initialBitrateEstimate, slidingWindowMaxWeight, clock); + if (eventHandler != null && eventListener != null) { + bandwidthMeter.addEventListener(eventHandler, eventListener); + } + return bandwidthMeter; } } private static final int ELAPSED_MILLIS_FOR_ESTIMATE = 2000; private static final int BYTES_TRANSFERRED_FOR_ESTIMATE = 512 * 1024; - private final @Nullable Handler eventHandler; - private final @Nullable EventListener eventListener; + private final EventDispatcher eventDispatcher; private final SlidingPercentile slidingPercentile; private final Clock clock; @@ -128,39 +132,32 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList /** Creates a bandwidth meter with default parameters. */ public DefaultBandwidthMeter() { - this( - /* eventHandler= */ null, - /* eventListener= */ null, - DEFAULT_INITIAL_BITRATE_ESTIMATE, - DEFAULT_SLIDING_WINDOW_MAX_WEIGHT, - Clock.DEFAULT); + this(DEFAULT_INITIAL_BITRATE_ESTIMATE, DEFAULT_SLIDING_WINDOW_MAX_WEIGHT, Clock.DEFAULT); } /** @deprecated Use {@link Builder} instead. */ @Deprecated public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener) { - this( - eventHandler, - eventListener, - DEFAULT_INITIAL_BITRATE_ESTIMATE, - DEFAULT_SLIDING_WINDOW_MAX_WEIGHT, - Clock.DEFAULT); + this(DEFAULT_INITIAL_BITRATE_ESTIMATE, DEFAULT_SLIDING_WINDOW_MAX_WEIGHT, Clock.DEFAULT); + if (eventHandler != null && eventListener != null) { + addEventListener(eventHandler, eventListener); + } } /** @deprecated Use {@link Builder} instead. */ @Deprecated public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, int maxWeight) { - this(eventHandler, eventListener, DEFAULT_INITIAL_BITRATE_ESTIMATE, maxWeight, Clock.DEFAULT); + this(DEFAULT_INITIAL_BITRATE_ESTIMATE, maxWeight, Clock.DEFAULT); + if (eventHandler != null && eventListener != null) { + addEventListener(eventHandler, eventListener); + } } private DefaultBandwidthMeter( - @Nullable Handler eventHandler, - @Nullable EventListener eventListener, long initialBitrateEstimate, int maxWeight, Clock clock) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; + this.eventDispatcher = new EventDispatcher<>(); this.slidingPercentile = new SlidingPercentile(maxWeight); this.clock = clock; bitrateEstimate = initialBitrateEstimate; @@ -172,7 +169,31 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList } @Override - public synchronized void onTransferStart(Object source, DataSpec dataSpec) { + public @Nullable TransferListener getTransferListener() { + return this; + } + + @Override + public void addEventListener(Handler eventHandler, EventListener eventListener) { + eventDispatcher.addListener(eventHandler, eventListener); + } + + @Override + public void removeEventListener(EventListener eventListener) { + eventDispatcher.removeListener(eventListener); + } + + @Override + public void onTransferInitializing(DataSource source, DataSpec dataSpec, boolean isNetwork) { + // Do nothing. + } + + @Override + public synchronized void onTransferStart( + DataSource source, DataSpec dataSpec, boolean isNetwork) { + if (!isNetwork) { + return; + } if (streamCount == 0) { sampleStartTimeMs = clock.elapsedRealtime(); } @@ -180,12 +201,19 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList } @Override - public synchronized void onBytesTransferred(Object source, int bytes) { + public synchronized void onBytesTransferred( + DataSource source, DataSpec dataSpec, boolean isNetwork, int bytes) { + if (!isNetwork) { + return; + } sampleBytesTransferred += bytes; } @Override - public synchronized void onTransferEnd(Object source) { + public synchronized void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork) { + if (!isNetwork) { + return; + } Assertions.checkState(streamCount > 0); long nowMs = clock.elapsedRealtime(); int sampleElapsedTimeMs = (int) (nowMs - sampleStartTimeMs); @@ -206,14 +234,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList sampleBytesTransferred = 0; } - private void notifyBandwidthSample(final int elapsedMs, final long bytes, final long bitrate) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onBandwidthSample(elapsedMs, bytes, bitrate); - } - }); - } + private void notifyBandwidthSample(int elapsedMs, long bytes, long bitrate) { + eventDispatcher.dispatch(listener -> listener.onBandwidthSample(elapsedMs, bytes, bitrate)); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java index b5469db72e..23d6cc368f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -17,10 +17,14 @@ package com.google.android.exoplayer2.upstream; import android.content.Context; import android.net.Uri; +import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * A {@link DataSource} that supports multiple URI schemes. The supported schemes are: @@ -53,31 +57,96 @@ public final class DefaultDataSource implements DataSource { private static final String SCHEME_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME; private final Context context; - private final TransferListener listener; - + private final List transferListeners; private final DataSource baseDataSource; // Lazily initialized. - private DataSource fileDataSource; - private DataSource assetDataSource; - private DataSource contentDataSource; - private DataSource rtmpDataSource; - private DataSource dataSchemeDataSource; - private DataSource rawResourceDataSource; + private @Nullable DataSource fileDataSource; + private @Nullable DataSource assetDataSource; + private @Nullable DataSource contentDataSource; + private @Nullable DataSource rtmpDataSource; + private @Nullable DataSource dataSchemeDataSource; + private @Nullable DataSource rawResourceDataSource; - private DataSource dataSource; + private @Nullable DataSource dataSource; + + /** + * Constructs a new instance, optionally configured to follow cross-protocol redirects. + * + * @param context A context. + * @param userAgent The User-Agent to use when requesting remote data. + * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP + * to HTTPS and vice versa) are enabled when fetching remote data. + */ + public DefaultDataSource(Context context, String userAgent, boolean allowCrossProtocolRedirects) { + this( + context, + userAgent, + DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, + DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, + allowCrossProtocolRedirects); + } + + /** + * Constructs a new instance, optionally configured to follow cross-protocol redirects. + * + * @param context A context. + * @param userAgent The User-Agent to use when requesting remote data. + * @param connectTimeoutMillis The connection timeout that should be used when requesting remote + * data, in milliseconds. A timeout of zero is interpreted as an infinite timeout. + * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in + * milliseconds. A timeout of zero is interpreted as an infinite timeout. + * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP + * to HTTPS and vice versa) are enabled when fetching remote data. + */ + public DefaultDataSource( + Context context, + String userAgent, + int connectTimeoutMillis, + int readTimeoutMillis, + boolean allowCrossProtocolRedirects) { + this( + context, + new DefaultHttpDataSource( + userAgent, + /* contentTypePredicate= */ null, + connectTimeoutMillis, + readTimeoutMillis, + allowCrossProtocolRedirects, + /* defaultRequestProperties= */ null)); + } + + /** + * Constructs a new instance that delegates to a provided {@link DataSource} for URI schemes other + * than file, asset and content. + * + * @param context A context. + * @param baseDataSource A {@link DataSource} to use for URI schemes other than file, asset and + * content. This {@link DataSource} should normally support at least http(s). + */ + public DefaultDataSource(Context context, DataSource baseDataSource) { + this.context = context.getApplicationContext(); + this.baseDataSource = Assertions.checkNotNull(baseDataSource); + transferListeners = new ArrayList<>(); + } /** * Constructs a new instance, optionally configured to follow cross-protocol redirects. * * @param context A context. * @param listener An optional listener. - * @param userAgent The User-Agent string that should be used when requesting remote data. + * @param userAgent The User-Agent to use when requesting remote data. * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP * to HTTPS and vice versa) are enabled when fetching remote data. + * @deprecated Use {@link #DefaultDataSource(Context, String, boolean)} and {@link + * #addTransferListener(TransferListener)}. */ - public DefaultDataSource(Context context, TransferListener listener, - String userAgent, boolean allowCrossProtocolRedirects) { + @Deprecated + public DefaultDataSource( + Context context, + @Nullable TransferListener listener, + String userAgent, + boolean allowCrossProtocolRedirects) { this(context, listener, userAgent, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, allowCrossProtocolRedirects); } @@ -87,20 +156,35 @@ public final class DefaultDataSource implements DataSource { * * @param context A context. * @param listener An optional listener. - * @param userAgent The User-Agent string that should be used when requesting remote data. + * @param userAgent The User-Agent to use when requesting remote data. * @param connectTimeoutMillis The connection timeout that should be used when requesting remote * data, in milliseconds. A timeout of zero is interpreted as an infinite timeout. - * @param readTimeoutMillis The read timeout that should be used when requesting remote data, - * in milliseconds. A timeout of zero is interpreted as an infinite timeout. + * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in + * milliseconds. A timeout of zero is interpreted as an infinite timeout. * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP * to HTTPS and vice versa) are enabled when fetching remote data. + * @deprecated Use {@link #DefaultDataSource(Context, String, int, int, boolean)} and {@link + * #addTransferListener(TransferListener)}. */ - public DefaultDataSource(Context context, TransferListener listener, - String userAgent, int connectTimeoutMillis, int readTimeoutMillis, + @Deprecated + public DefaultDataSource( + Context context, + @Nullable TransferListener listener, + String userAgent, + int connectTimeoutMillis, + int readTimeoutMillis, boolean allowCrossProtocolRedirects) { - this(context, listener, - new DefaultHttpDataSource(userAgent, null, listener, connectTimeoutMillis, - readTimeoutMillis, allowCrossProtocolRedirects, null)); + this( + context, + listener, + new DefaultHttpDataSource( + userAgent, + /* contentTypePredicate= */ null, + listener, + connectTimeoutMillis, + readTimeoutMillis, + allowCrossProtocolRedirects, + /* defaultRequestProperties= */ null)); } /** @@ -111,12 +195,28 @@ public final class DefaultDataSource implements DataSource { * @param listener An optional listener. * @param baseDataSource A {@link DataSource} to use for URI schemes other than file, asset and * content. This {@link DataSource} should normally support at least http(s). + * @deprecated Use {@link #DefaultDataSource(Context, DataSource)} and {@link + * #addTransferListener(TransferListener)}. */ - public DefaultDataSource(Context context, TransferListener listener, - DataSource baseDataSource) { - this.context = context.getApplicationContext(); - this.listener = listener; - this.baseDataSource = Assertions.checkNotNull(baseDataSource); + @Deprecated + public DefaultDataSource( + Context context, @Nullable TransferListener listener, DataSource baseDataSource) { + this(context, baseDataSource); + if (listener != null) { + transferListeners.add(listener); + } + } + + @Override + public void addTransferListener(TransferListener transferListener) { + baseDataSource.addTransferListener(transferListener); + transferListeners.add(transferListener); + maybeAddListenerToDataSource(fileDataSource, transferListener); + maybeAddListenerToDataSource(assetDataSource, transferListener); + maybeAddListenerToDataSource(contentDataSource, transferListener); + maybeAddListenerToDataSource(rtmpDataSource, transferListener); + maybeAddListenerToDataSource(dataSchemeDataSource, transferListener); + maybeAddListenerToDataSource(rawResourceDataSource, transferListener); } @Override @@ -149,14 +249,21 @@ public final class DefaultDataSource implements DataSource { @Override public int read(byte[] buffer, int offset, int readLength) throws IOException { - return dataSource.read(buffer, offset, readLength); + return Assertions.checkNotNull(dataSource).read(buffer, offset, readLength); } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return dataSource == null ? null : dataSource.getUri(); } + @Override + public Map> getResponseHeaders() { + return dataSource == null + ? DataSource.super.getResponseHeaders() + : dataSource.getResponseHeaders(); + } + @Override public void close() throws IOException { if (dataSource != null) { @@ -170,21 +277,24 @@ public final class DefaultDataSource implements DataSource { private DataSource getFileDataSource() { if (fileDataSource == null) { - fileDataSource = new FileDataSource(listener); + fileDataSource = new FileDataSource(); + addListenersToDataSource(fileDataSource); } return fileDataSource; } private DataSource getAssetDataSource() { if (assetDataSource == null) { - assetDataSource = new AssetDataSource(context, listener); + assetDataSource = new AssetDataSource(context); + addListenersToDataSource(assetDataSource); } return assetDataSource; } private DataSource getContentDataSource() { if (contentDataSource == null) { - contentDataSource = new ContentDataSource(context, listener); + contentDataSource = new ContentDataSource(context); + addListenersToDataSource(contentDataSource); } return contentDataSource; } @@ -196,6 +306,7 @@ public final class DefaultDataSource implements DataSource { Class clazz = Class.forName("com.google.android.exoplayer2.ext.rtmp.RtmpDataSource"); rtmpDataSource = (DataSource) clazz.getConstructor().newInstance(); // LINT.ThenChange(../../../../../../../../proguard-rules.txt) + addListenersToDataSource(rtmpDataSource); } catch (ClassNotFoundException e) { // Expected if the app was built without the RTMP extension. Log.w(TAG, "Attempting to play RTMP stream without depending on the RTMP extension"); @@ -213,14 +324,29 @@ public final class DefaultDataSource implements DataSource { private DataSource getDataSchemeDataSource() { if (dataSchemeDataSource == null) { dataSchemeDataSource = new DataSchemeDataSource(); + addListenersToDataSource(dataSchemeDataSource); } return dataSchemeDataSource; } private DataSource getRawResourceDataSource() { if (rawResourceDataSource == null) { - rawResourceDataSource = new RawResourceDataSource(context, listener); + rawResourceDataSource = new RawResourceDataSource(context); + addListenersToDataSource(rawResourceDataSource); } return rawResourceDataSource; } + + private void addListenersToDataSource(DataSource dataSource) { + for (int i = 0; i < transferListeners.size(); i++) { + dataSource.addTransferListener(transferListeners.get(i)); + } + } + + private void maybeAddListenerToDataSource( + @Nullable DataSource dataSource, TransferListener listener) { + if (dataSource != null) { + dataSource.addTransferListener(listener); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java index 34d60e4b81..293ba7f17b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream; import android.content.Context; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.upstream.DataSource.Factory; /** @@ -25,7 +26,7 @@ import com.google.android.exoplayer2.upstream.DataSource.Factory; public final class DefaultDataSourceFactory implements Factory { private final Context context; - private final TransferListener listener; + private final @Nullable TransferListener listener; private final DataSource.Factory baseDataSourceFactory; /** @@ -33,7 +34,7 @@ public final class DefaultDataSourceFactory implements Factory { * @param userAgent The User-Agent string that should be used. */ public DefaultDataSourceFactory(Context context, String userAgent) { - this(context, userAgent, null); + this(context, userAgent, /* listener= */ null); } /** @@ -41,11 +42,21 @@ public final class DefaultDataSourceFactory implements Factory { * @param userAgent The User-Agent string that should be used. * @param listener An optional listener. */ - public DefaultDataSourceFactory(Context context, String userAgent, - TransferListener listener) { + public DefaultDataSourceFactory( + Context context, String userAgent, @Nullable TransferListener listener) { this(context, listener, new DefaultHttpDataSourceFactory(userAgent, listener)); } + /** + * @param context A context. + * @param baseDataSourceFactory A {@link Factory} to be used to create a base {@link DataSource} + * for {@link DefaultDataSource}. + * @see DefaultDataSource#DefaultDataSource(Context, TransferListener, DataSource) + */ + public DefaultDataSourceFactory(Context context, DataSource.Factory baseDataSourceFactory) { + this(context, /* listener= */ null, baseDataSourceFactory); + } + /** * @param context A context. * @param listener An optional listener. @@ -53,7 +64,9 @@ public final class DefaultDataSourceFactory implements Factory { * for {@link DefaultDataSource}. * @see DefaultDataSource#DefaultDataSource(Context, TransferListener, DataSource) */ - public DefaultDataSourceFactory(Context context, TransferListener listener, + public DefaultDataSourceFactory( + Context context, + @Nullable TransferListener listener, DataSource.Factory baseDataSourceFactory) { this.context = context.getApplicationContext(); this.listener = listener; @@ -64,5 +77,4 @@ public final class DefaultDataSourceFactory implements Factory { public DefaultDataSource createDataSource() { return new DefaultDataSource(context, listener, baseDataSourceFactory.createDataSource()); } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index a47a5b5348..87ea36bd18 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -16,9 +16,11 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Predicate; import com.google.android.exoplayer2.util.Util; @@ -32,6 +34,7 @@ import java.net.HttpURLConnection; import java.net.NoRouteToHostException; import java.net.ProtocolException; import java.net.URL; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -40,13 +43,13 @@ import java.util.regex.Pattern; /** * An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}. - *

    - * By default this implementation will not follow cross-protocol redirects (i.e. redirects from - * HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the - * {@link #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean, + * + *

    By default this implementation will not follow cross-protocol redirects (i.e. redirects from + * HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the {@link + * #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean, * RequestProperties)} constructor and passing {@code true} as the second last argument. */ -public class DefaultHttpDataSource implements HttpDataSource { +public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSource { /** * The default connection timeout, in milliseconds. @@ -59,6 +62,8 @@ public class DefaultHttpDataSource implements HttpDataSource { private static final String TAG = "DefaultHttpDataSource"; private static final int MAX_REDIRECTS = 20; // Same limit as okhttp. + private static final int HTTP_STATUS_TEMPORARY_REDIRECT = 307; + private static final int HTTP_STATUS_PERMANENT_REDIRECT = 308; private static final long MAX_BYTES_TO_DRAIN = 2048; private static final Pattern CONTENT_RANGE_HEADER = Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$"); @@ -68,14 +73,13 @@ public class DefaultHttpDataSource implements HttpDataSource { private final int connectTimeoutMillis; private final int readTimeoutMillis; private final String userAgent; - private final Predicate contentTypePredicate; - private final RequestProperties defaultRequestProperties; + private final @Nullable Predicate contentTypePredicate; + private final @Nullable RequestProperties defaultRequestProperties; private final RequestProperties requestProperties; - private final TransferListener listener; - private DataSpec dataSpec; - private HttpURLConnection connection; - private InputStream inputStream; + private @Nullable DataSpec dataSpec; + private @Nullable HttpURLConnection connection; + private @Nullable InputStream inputStream; private boolean opened; private long bytesToSkip; @@ -87,22 +91,87 @@ public class DefaultHttpDataSource implements HttpDataSource { /** * @param userAgent The User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. + * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. */ - public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate) { - this(userAgent, contentTypePredicate, null); + public DefaultHttpDataSource(String userAgent, @Nullable Predicate contentTypePredicate) { + this( + userAgent, + contentTypePredicate, + DEFAULT_CONNECT_TIMEOUT_MILLIS, + DEFAULT_READ_TIMEOUT_MILLIS); } /** * @param userAgent The User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. - * @param listener An optional listener. + * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. + * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is + * interpreted as an infinite timeout. + * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as + * an infinite timeout. */ - public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, - TransferListener listener) { + public DefaultHttpDataSource( + String userAgent, + @Nullable Predicate contentTypePredicate, + int connectTimeoutMillis, + int readTimeoutMillis) { + this( + userAgent, + contentTypePredicate, + connectTimeoutMillis, + readTimeoutMillis, + /* allowCrossProtocolRedirects= */ false, + /* defaultRequestProperties= */ null); + } + + /** + * @param userAgent The User-Agent string that should be used. + * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the + * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. + * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is + * interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use the + * default value. + * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as + * an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value. + * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP + * to HTTPS and vice versa) are enabled. + * @param defaultRequestProperties The default request properties to be sent to the server as HTTP + * headers or {@code null} if not required. + */ + public DefaultHttpDataSource( + String userAgent, + @Nullable Predicate contentTypePredicate, + int connectTimeoutMillis, + int readTimeoutMillis, + boolean allowCrossProtocolRedirects, + @Nullable RequestProperties defaultRequestProperties) { + super(/* isNetwork= */ true); + this.userAgent = Assertions.checkNotEmpty(userAgent); + this.contentTypePredicate = contentTypePredicate; + this.requestProperties = new RequestProperties(); + this.connectTimeoutMillis = connectTimeoutMillis; + this.readTimeoutMillis = readTimeoutMillis; + this.allowCrossProtocolRedirects = allowCrossProtocolRedirects; + this.defaultRequestProperties = defaultRequestProperties; + } + + /** + * @param userAgent The User-Agent string that should be used. + * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the + * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. + * @param listener An optional listener. + * @deprecated Use {@link #DefaultHttpDataSource(String, Predicate)} and {@link + * #addTransferListener(TransferListener)}. + */ + @Deprecated + public DefaultHttpDataSource( + String userAgent, + @Nullable Predicate contentTypePredicate, + @Nullable TransferListener listener) { this(userAgent, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS); } @@ -110,16 +179,22 @@ public class DefaultHttpDataSource implements HttpDataSource { /** * @param userAgent The User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. + * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. * @param listener An optional listener. * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is * interpreted as an infinite timeout. - * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted - * as an infinite timeout. + * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as + * an infinite timeout. + * @deprecated Use {@link #DefaultHttpDataSource(String, Predicate, int, int)} and {@link + * #addTransferListener(TransferListener)}. */ - public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, - TransferListener listener, int connectTimeoutMillis, + @Deprecated + public DefaultHttpDataSource( + String userAgent, + @Nullable Predicate contentTypePredicate, + @Nullable TransferListener listener, + int connectTimeoutMillis, int readTimeoutMillis) { this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false, null); @@ -128,41 +203,50 @@ public class DefaultHttpDataSource implements HttpDataSource { /** * @param userAgent The User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. + * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. * @param listener An optional listener. * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is - * interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use - * the default value. - * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted - * as an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value. + * interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use the + * default value. + * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as + * an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value. * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP * to HTTPS and vice versa) are enabled. - * @param defaultRequestProperties The default request properties to be sent to the server as - * HTTP headers or {@code null} if not required. + * @param defaultRequestProperties The default request properties to be sent to the server as HTTP + * headers or {@code null} if not required. + * @deprecated Use {@link #DefaultHttpDataSource(String, Predicate, int, int, boolean, + * RequestProperties)} and {@link #addTransferListener(TransferListener)}. */ - public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, - TransferListener listener, int connectTimeoutMillis, - int readTimeoutMillis, boolean allowCrossProtocolRedirects, - RequestProperties defaultRequestProperties) { - this.userAgent = Assertions.checkNotEmpty(userAgent); - this.contentTypePredicate = contentTypePredicate; - this.listener = listener; - this.requestProperties = new RequestProperties(); - this.connectTimeoutMillis = connectTimeoutMillis; - this.readTimeoutMillis = readTimeoutMillis; - this.allowCrossProtocolRedirects = allowCrossProtocolRedirects; - this.defaultRequestProperties = defaultRequestProperties; + @Deprecated + public DefaultHttpDataSource( + String userAgent, + @Nullable Predicate contentTypePredicate, + @Nullable TransferListener listener, + int connectTimeoutMillis, + int readTimeoutMillis, + boolean allowCrossProtocolRedirects, + @Nullable RequestProperties defaultRequestProperties) { + this( + userAgent, + contentTypePredicate, + connectTimeoutMillis, + readTimeoutMillis, + allowCrossProtocolRedirects, + defaultRequestProperties); + if (listener != null) { + addTransferListener(listener); + } } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return connection == null ? null : Uri.parse(connection.getURL().toString()); } @Override public Map> getResponseHeaders() { - return connection == null ? null : connection.getHeaderFields(); + return connection == null ? Collections.emptyMap() : connection.getHeaderFields(); } @Override @@ -188,6 +272,7 @@ public class DefaultHttpDataSource implements HttpDataSource { this.dataSpec = dataSpec; this.bytesRead = 0; this.bytesSkipped = 0; + transferInitializing(dataSpec); try { connection = makeConnection(dataSpec); } catch (IOException e) { @@ -253,9 +338,7 @@ public class DefaultHttpDataSource implements HttpDataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesToRead; } @@ -286,9 +369,7 @@ public class DefaultHttpDataSource implements HttpDataSource { closeConnectionQuietly(); if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } } @@ -339,7 +420,8 @@ public class DefaultHttpDataSource implements HttpDataSource { */ private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { URL url = new URL(dataSpec.uri.toString()); - byte[] postBody = dataSpec.postBody; + @HttpMethod int httpMethod = dataSpec.httpMethod; + byte[] httpBody = dataSpec.httpBody; long position = dataSpec.position; long length = dataSpec.length; boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP); @@ -347,28 +429,37 @@ public class DefaultHttpDataSource implements HttpDataSource { if (!allowCrossProtocolRedirects) { // HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection // automatically. This is the behavior we want, so use it. - return makeConnection(url, postBody, position, length, allowGzip, true /* followRedirects */); + return makeConnection( + url, httpMethod, httpBody, position, length, allowGzip, true /* followRedirects */); } // We need to handle redirects ourselves to allow cross-protocol redirects. int redirectCount = 0; while (redirectCount++ <= MAX_REDIRECTS) { - HttpURLConnection connection = makeConnection( - url, postBody, position, length, allowGzip, false /* followRedirects */); + HttpURLConnection connection = + makeConnection( + url, httpMethod, httpBody, position, length, allowGzip, false /* followRedirects */); int responseCode = connection.getResponseCode(); - if (responseCode == HttpURLConnection.HTTP_MULT_CHOICE - || responseCode == HttpURLConnection.HTTP_MOVED_PERM - || responseCode == HttpURLConnection.HTTP_MOVED_TEMP - || responseCode == HttpURLConnection.HTTP_SEE_OTHER - || (postBody == null - && (responseCode == 307 /* HTTP_TEMP_REDIRECT */ - || responseCode == 308 /* HTTP_PERM_REDIRECT */))) { - // For 300, 301, 302, and 303 POST requests follow the redirect and are transformed into - // GET requests. For 307 and 308 POST requests are not redirected. - postBody = null; - String location = connection.getHeaderField("Location"); + String location = connection.getHeaderField("Location"); + if ((httpMethod == DataSpec.HTTP_METHOD_GET || httpMethod == DataSpec.HTTP_METHOD_HEAD) + && (responseCode == HttpURLConnection.HTTP_MULT_CHOICE + || responseCode == HttpURLConnection.HTTP_MOVED_PERM + || responseCode == HttpURLConnection.HTTP_MOVED_TEMP + || responseCode == HttpURLConnection.HTTP_SEE_OTHER + || responseCode == HTTP_STATUS_TEMPORARY_REDIRECT + || responseCode == HTTP_STATUS_PERMANENT_REDIRECT)) { connection.disconnect(); url = handleRedirect(url, location); + } else if (httpMethod == DataSpec.HTTP_METHOD_POST + && (responseCode == HttpURLConnection.HTTP_MULT_CHOICE + || responseCode == HttpURLConnection.HTTP_MOVED_PERM + || responseCode == HttpURLConnection.HTTP_MOVED_TEMP + || responseCode == HttpURLConnection.HTTP_SEE_OTHER)) { + // POST request follows the redirect and is transformed into a GET request. + connection.disconnect(); + httpMethod = DataSpec.HTTP_METHOD_GET; + httpBody = null; + url = handleRedirect(url, location); } else { return connection; } @@ -382,14 +473,22 @@ public class DefaultHttpDataSource implements HttpDataSource { * Configures a connection and opens it. * * @param url The url to connect to. - * @param postBody The body data for a POST request. + * @param httpMethod The http method. + * @param httpBody The body data. * @param position The byte offset of the requested data. * @param length The length of the requested data, or {@link C#LENGTH_UNSET}. * @param allowGzip Whether to allow the use of gzip. * @param followRedirects Whether to follow redirects. */ - private HttpURLConnection makeConnection(URL url, byte[] postBody, long position, - long length, boolean allowGzip, boolean followRedirects) throws IOException { + private HttpURLConnection makeConnection( + URL url, + @HttpMethod int httpMethod, + byte[] httpBody, + long position, + long length, + boolean allowGzip, + boolean followRedirects) + throws IOException { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(connectTimeoutMillis); connection.setReadTimeout(readTimeoutMillis); @@ -413,18 +512,14 @@ public class DefaultHttpDataSource implements HttpDataSource { connection.setRequestProperty("Accept-Encoding", "identity"); } connection.setInstanceFollowRedirects(followRedirects); - connection.setDoOutput(postBody != null); - if (postBody != null) { - connection.setRequestMethod("POST"); - if (postBody.length == 0) { - connection.connect(); - } else { - connection.setFixedLengthStreamingMode(postBody.length); - connection.connect(); - OutputStream os = connection.getOutputStream(); - os.write(postBody); - os.close(); - } + connection.setDoOutput(httpBody != null); + connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod)); + if (httpBody != null) { + connection.setFixedLengthStreamingMode(httpBody.length); + connection.connect(); + OutputStream os = connection.getOutputStream(); + os.write(httpBody); + os.close(); } else { connection.connect(); } @@ -533,9 +628,7 @@ public class DefaultHttpDataSource implements HttpDataSource { throw new EOFException(); } bytesSkipped += read; - if (listener != null) { - listener.onBytesTransferred(this, read); - } + bytesTransferred(read); } // Release the shared skip buffer. @@ -578,9 +671,7 @@ public class DefaultHttpDataSource implements HttpDataSource { } bytesRead += read; - if (listener != null) { - listener.onBytesTransferred(this, read); - } + bytesTransferred(read); return read; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java index 3b3a5a1c16..aa0ac7b97e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; @@ -22,7 +23,7 @@ import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; public final class DefaultHttpDataSourceFactory extends BaseFactory { private final String userAgent; - private final TransferListener listener; + private final @Nullable TransferListener listener; private final int connectTimeoutMillis; private final int readTimeoutMillis; private final boolean allowCrossProtocolRedirects; @@ -49,12 +50,33 @@ public final class DefaultHttpDataSourceFactory extends BaseFactory { * @param listener An optional listener. * @see #DefaultHttpDataSourceFactory(String, TransferListener, int, int, boolean) */ - public DefaultHttpDataSourceFactory( - String userAgent, TransferListener listener) { + public DefaultHttpDataSourceFactory(String userAgent, @Nullable TransferListener listener) { this(userAgent, listener, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, false); } + /** + * @param userAgent The User-Agent string that should be used. + * @param connectTimeoutMillis The connection timeout that should be used when requesting remote + * data, in milliseconds. A timeout of zero is interpreted as an infinite timeout. + * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in + * milliseconds. A timeout of zero is interpreted as an infinite timeout. + * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP + * to HTTPS and vice versa) are enabled. + */ + public DefaultHttpDataSourceFactory( + String userAgent, + int connectTimeoutMillis, + int readTimeoutMillis, + boolean allowCrossProtocolRedirects) { + this( + userAgent, + /* listener= */ null, + connectTimeoutMillis, + readTimeoutMillis, + allowCrossProtocolRedirects); + } + /** * @param userAgent The User-Agent string that should be used. * @param listener An optional listener. @@ -65,9 +87,12 @@ public final class DefaultHttpDataSourceFactory extends BaseFactory { * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP * to HTTPS and vice versa) are enabled. */ - public DefaultHttpDataSourceFactory(String userAgent, - TransferListener listener, int connectTimeoutMillis, - int readTimeoutMillis, boolean allowCrossProtocolRedirects) { + public DefaultHttpDataSourceFactory( + String userAgent, + @Nullable TransferListener listener, + int connectTimeoutMillis, + int readTimeoutMillis, + boolean allowCrossProtocolRedirects) { this.userAgent = userAgent; this.listener = listener; this.connectTimeoutMillis = connectTimeoutMillis; @@ -81,5 +106,4 @@ public final class DefaultHttpDataSourceFactory extends BaseFactory { return new DefaultHttpDataSource(userAgent, null, listener, connectTimeoutMillis, readTimeoutMillis, allowCrossProtocolRedirects, defaultRequestProperties); } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java new file mode 100644 index 0000000000..4eec495cab --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.upstream; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException; +import java.io.IOException; + +/** Default implementation of {@link LoadErrorHandlingPolicy}. */ +public final class DefaultLoadErrorHandlingPolicy implements LoadErrorHandlingPolicy { + + /** The default minimum number of times to retry loading data prior to propagating the error. */ + public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; + /** + * The default minimum number of times to retry loading prior to failing for progressive live + * streams. + */ + public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT_PROGRESSIVE_LIVE = 6; + /** The default duration for which a track is blacklisted in milliseconds. */ + public static final long DEFAULT_TRACK_BLACKLIST_MS = 60000; + + private static final int DEFAULT_BEHAVIOR_MIN_LOADABLE_RETRY_COUNT = -1; + + private final int minimumLoadableRetryCount; + + /** + * Creates an instance with default behavior. + * + *

    {@link #getMinimumLoadableRetryCount} will return {@link + * #DEFAULT_MIN_LOADABLE_RETRY_COUNT_PROGRESSIVE_LIVE} for {@code dataType} {@link + * C#DATA_TYPE_MEDIA_PROGRESSIVE_LIVE}. For other {@code dataType} values, it will return {@link + * #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + */ + public DefaultLoadErrorHandlingPolicy() { + this(DEFAULT_BEHAVIOR_MIN_LOADABLE_RETRY_COUNT); + } + + /** + * Creates an instance with the given value for {@link #getMinimumLoadableRetryCount(int)}. + * + * @param minimumLoadableRetryCount See {@link #getMinimumLoadableRetryCount}. + */ + public DefaultLoadErrorHandlingPolicy(int minimumLoadableRetryCount) { + this.minimumLoadableRetryCount = minimumLoadableRetryCount; + } + + /** + * Blacklists resources whose load error was an {@link InvalidResponseCodeException} with response + * code HTTP 404 or 410. The duration of the blacklisting is {@link #DEFAULT_TRACK_BLACKLIST_MS}. + */ + @Override + public long getBlacklistDurationMsFor( + int dataType, long loadDurationMs, IOException exception, int errorCount) { + if (exception instanceof InvalidResponseCodeException) { + int responseCode = ((InvalidResponseCodeException) exception).responseCode; + return responseCode == 404 // HTTP 404 Not Found. + || responseCode == 410 // HTTP 410 Gone. + ? DEFAULT_TRACK_BLACKLIST_MS + : C.TIME_UNSET; + } + return C.TIME_UNSET; + } + + /** + * Retries for any exception that is not a subclass of {@link ParserException}. The retry delay is + * calculated as {@code Math.min((errorCount - 1) * 1000, 5000)}. + */ + @Override + public long getRetryDelayMsFor( + int dataType, long loadDurationMs, IOException exception, int errorCount) { + return exception instanceof ParserException + ? C.TIME_UNSET + : Math.min((errorCount - 1) * 1000, 5000); + } + + /** + * See {@link #DefaultLoadErrorHandlingPolicy()} and {@link #DefaultLoadErrorHandlingPolicy(int)} + * for documentation about the behavior of this method. + */ + @Override + public int getMinimumLoadableRetryCount(int dataType) { + if (minimumLoadableRetryCount == DEFAULT_BEHAVIOR_MIN_LOADABLE_RETRY_COUNT) { + return dataType == C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE + ? DEFAULT_MIN_LOADABLE_RETRY_COUNT_PROGRESSIVE_LIVE + : DEFAULT_MIN_LOADABLE_RETRY_COUNT; + } else { + return minimumLoadableRetryCount; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java index fa3e14f1c9..06dc79e345 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.support.annotation.Nullable; import java.io.IOException; /** @@ -35,6 +36,11 @@ public final class DummyDataSource implements DataSource { private DummyDataSource() {} + @Override + public void addTransferListener(TransferListener transferListener) { + // Do nothing. + } + @Override public long open(DataSpec dataSpec) throws IOException { throw new IOException("Dummy source"); @@ -46,7 +52,7 @@ public final class DummyDataSource implements DataSource { } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return null; } @@ -54,5 +60,4 @@ public final class DummyDataSource implements DataSource { public void close() throws IOException { // do nothing. } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java index 898d2169b3..582b2b06da 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java @@ -16,15 +16,14 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.io.EOFException; import java.io.IOException; import java.io.RandomAccessFile; -/** - * A {@link DataSource} for reading local files. - */ -public final class FileDataSource implements DataSource { +/** A {@link DataSource} for reading local files. */ +public final class FileDataSource extends BaseDataSource { /** * Thrown when IOException is encountered during local file read operation. @@ -37,28 +36,32 @@ public final class FileDataSource implements DataSource { } - private final TransferListener listener; - - private RandomAccessFile file; - private Uri uri; + private @Nullable RandomAccessFile file; + private @Nullable Uri uri; private long bytesRemaining; private boolean opened; public FileDataSource() { - this(null); + super(/* isNetwork= */ false); } /** * @param listener An optional listener. + * @deprecated Use {@link #FileDataSource()} and {@link #addTransferListener(TransferListener)} */ - public FileDataSource(TransferListener listener) { - this.listener = listener; + @Deprecated + public FileDataSource(@Nullable TransferListener listener) { + this(); + if (listener != null) { + addTransferListener(listener); + } } @Override public long open(DataSpec dataSpec) throws FileDataSourceException { try { uri = dataSpec.uri; + transferInitializing(dataSpec); file = new RandomAccessFile(dataSpec.uri.getPath(), "r"); file.seek(dataSpec.position); bytesRemaining = dataSpec.length == C.LENGTH_UNSET ? file.length() - dataSpec.position @@ -71,9 +74,7 @@ public final class FileDataSource implements DataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesRemaining; } @@ -94,9 +95,7 @@ public final class FileDataSource implements DataSource { if (bytesRead > 0) { bytesRemaining -= bytesRead; - if (listener != null) { - listener.onBytesTransferred(this, bytesRead); - } + bytesTransferred(bytesRead); } return bytesRead; @@ -104,7 +103,7 @@ public final class FileDataSource implements DataSource { } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return uri; } @@ -121,9 +120,7 @@ public final class FileDataSource implements DataSource { file = null; if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java index 2accbfc584..f69adeb8c2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java @@ -15,18 +15,20 @@ */ package com.google.android.exoplayer2.upstream; +import android.support.annotation.Nullable; + /** * A {@link DataSource.Factory} that produces {@link FileDataSource}. */ public final class FileDataSourceFactory implements DataSource.Factory { - private final TransferListener listener; + private final @Nullable TransferListener listener; public FileDataSourceFactory() { this(null); } - public FileDataSourceFactory(TransferListener listener) { + public FileDataSourceFactory(@Nullable TransferListener listener) { this.listener = listener; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java index 3725fc0052..71a0e68260 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java @@ -341,10 +341,6 @@ public interface HttpDataSource extends DataSource { */ void clearAllRequestProperties(); - /** - * Returns the headers provided in the response, or {@code null} if response headers are - * unavailable. - */ + @Override Map> getResponseHeaders(); - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java new file mode 100644 index 0000000000..3432935d5f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.upstream; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.Loader.Callback; +import com.google.android.exoplayer2.upstream.Loader.Loadable; +import java.io.IOException; + +/** + * Defines how errors encountered by {@link Loader Loaders} are handled. + * + *

    Loader clients may blacklist a resource when a load error occurs. Blacklisting works around + * load errors by loading an alternative resource. Clients do not try blacklisting when a resource + * does not have an alternative. When a resource does have valid alternatives, {@link + * #getBlacklistDurationMsFor(int, long, IOException, int)} defines whether the resource should be + * blacklisted. Blacklisting will succeed if any of the alternatives is not in the black list. + * + *

    When blacklisting does not take place, {@link #getRetryDelayMsFor(int, long, IOException, + * int)} defines whether the load is retried. Errors whose load is not retried are propagated. Load + * errors whose load is retried are propagated according to {@link + * #getMinimumLoadableRetryCount(int)}. + * + *

    Methods are invoked on the playback thread. + */ +public interface LoadErrorHandlingPolicy { + + /** + * Returns the number of milliseconds for which a resource associated to a provided load error + * should be blacklisted, or {@link C#TIME_UNSET} if the resource should not be blacklisted. + * + * @param dataType One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to + * load. + * @param loadDurationMs The duration in milliseconds of the load up to the point at which the + * error occurred, including any previous attempts. + * @param exception The load error. + * @param errorCount The number of errors this load has encountered, including this one. + * @return The blacklist duration in milliseconds, or {@link C#TIME_UNSET} if the resource should + * not be blacklisted. + */ + long getBlacklistDurationMsFor( + int dataType, long loadDurationMs, IOException exception, int errorCount); + + /** + * Returns the number of milliseconds to wait before attempting the load again, or {@link + * C#TIME_UNSET} if the error is fatal and should not be retried. + * + *

    {@link Loader} clients may ignore the retry delay returned by this method in order to wait + * for a specific event before retrying. However, the load is retried if and only if this method + * does not return {@link C#TIME_UNSET}. + * + * @param dataType One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to + * load. + * @param loadDurationMs The duration in milliseconds of the load up to the point at which the + * error occurred, including any previous attempts. + * @param exception The load error. + * @param errorCount The number of errors this load has encountered, including this one. + * @return The number of milliseconds to wait before attempting the load again, or {@link + * C#TIME_UNSET} if the error is fatal and should not be retried. + */ + long getRetryDelayMsFor(int dataType, long loadDurationMs, IOException exception, int errorCount); + + /** + * Returns the minimum number of times to retry a load in the case of a load error, before + * propagating the error. + * + * @param dataType One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to + * load. + * @return The minimum number of times to retry a load in the case of a load error, before + * propagating the error. + * @see Loader#startLoading(Loadable, Callback, int) + */ + int getMinimumLoadableRetryCount(int dataType); +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index 074fc095ea..e284310f9e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -23,6 +23,7 @@ import android.os.SystemClock; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.util.Log; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; @@ -57,11 +58,6 @@ public final class Loader implements LoaderErrorThrower { */ void cancelLoad(); - /** - * Returns whether the load has been canceled. - */ - boolean isLoadCanceled(); - /** * Performs the load, returning on completion or cancellation. * @@ -79,27 +75,29 @@ public final class Loader implements LoaderErrorThrower { /** * Called when a load has completed. - *

    - * Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting and - * this callback being called. + * + *

    Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting + * and this callback being called. * * @param loadable The loadable whose load has completed. * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the load ended. - * @param loadDurationMs The duration of the load. + * @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading} + * was called. */ void onLoadCompleted(T loadable, long elapsedRealtimeMs, long loadDurationMs); /** * Called when a load has been canceled. - *

    - * Note: If the {@link Loader} has not been released then there is guaranteed to be a memory - * barrier between {@link Loadable#load()} exiting and this callback being called. If the - * {@link Loader} has been released then this callback may be called before - * {@link Loadable#load()} exits. + * + *

    Note: If the {@link Loader} has not been released then there is guaranteed to be a memory + * barrier between {@link Loadable#load()} exiting and this callback being called. If the {@link + * Loader} has been released then this callback may be called before {@link Loadable#load()} + * exits. * * @param loadable The loadable whose load has been canceled. * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the load was canceled. - * @param loadDurationMs The duration of the load up to the point at which it was canceled. + * @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading} + * was called up to the point at which it was canceled. * @param released True if the load was canceled because the {@link Loader} was released. False * otherwise. */ @@ -113,14 +111,16 @@ public final class Loader implements LoaderErrorThrower { * * @param loadable The loadable whose load has encountered an error. * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the error occurred. - * @param loadDurationMs The duration of the load up to the point at which the error occurred. + * @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading} + * was called up to the point at which the error occurred. * @param error The load error. - * @return The desired retry action. One of {@link Loader#RETRY}, {@link - * Loader#RETRY_RESET_ERROR_COUNT}, {@link Loader#DONT_RETRY} and {@link - * Loader#DONT_RETRY_FATAL}. + * @param errorCount The number of errors this load has encountered, including this one. + * @return The desired error handling action. One of {@link Loader#RETRY}, {@link + * Loader#RETRY_RESET_ERROR_COUNT}, {@link Loader#DONT_RETRY}, {@link + * Loader#DONT_RETRY_FATAL} or a retry action created by {@link #createRetryAction}. */ - @RetryAction - int onLoadError(T loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error); + LoadErrorAction onLoadError( + T loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error, int errorCount); } /** @@ -135,15 +135,56 @@ public final class Loader implements LoaderErrorThrower { } - /** Actions that can be taken in response to a load error. */ + /** Types of action that can be taken in response to a load error. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({RETRY, RETRY_RESET_ERROR_COUNT, DONT_RETRY, DONT_RETRY_FATAL}) - public @interface RetryAction {} + @IntDef({ + ACTION_TYPE_RETRY, + ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT, + ACTION_TYPE_DONT_RETRY, + ACTION_TYPE_DONT_RETRY_FATAL + }) + private @interface RetryActionType {} - public static final int RETRY = 0; - public static final int RETRY_RESET_ERROR_COUNT = 1; - public static final int DONT_RETRY = 2; - public static final int DONT_RETRY_FATAL = 3; + private static final int ACTION_TYPE_RETRY = 0; + private static final int ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT = 1; + private static final int ACTION_TYPE_DONT_RETRY = 2; + private static final int ACTION_TYPE_DONT_RETRY_FATAL = 3; + + /** Retries the load using the default delay. */ + public static final LoadErrorAction RETRY = + createRetryAction(/* resetErrorCount= */ false, C.TIME_UNSET); + /** Retries the load using the default delay and resets the error count. */ + public static final LoadErrorAction RETRY_RESET_ERROR_COUNT = + createRetryAction(/* resetErrorCount= */ true, C.TIME_UNSET); + /** Discards the failed loading task and ignores any errors that have occurred. */ + public static final LoadErrorAction DONT_RETRY = + new LoadErrorAction(ACTION_TYPE_DONT_RETRY, C.TIME_UNSET); + /** + * Discards the failed load. The next call to {@link #maybeThrowError()} will throw the last load + * error. + */ + public static final LoadErrorAction DONT_RETRY_FATAL = + new LoadErrorAction(ACTION_TYPE_DONT_RETRY_FATAL, C.TIME_UNSET); + + /** + * Action that can be taken in response to {@link Callback#onLoadError(Loadable, long, long, + * IOException, int)}. + */ + public static final class LoadErrorAction { + + private final @RetryActionType int type; + private final long retryDelayMillis; + + private LoadErrorAction(@RetryActionType int type, long retryDelayMillis) { + this.type = type; + this.retryDelayMillis = retryDelayMillis; + } + + /** Returns whether this is a retry action. */ + public boolean isRetry() { + return type == ACTION_TYPE_RETRY || type == ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT; + } + } private final ExecutorService downloadExecutorService; @@ -157,6 +198,19 @@ public final class Loader implements LoaderErrorThrower { this.downloadExecutorService = Util.newSingleThreadExecutor(threadName); } + /** + * Creates a {@link LoadErrorAction} for retrying with the given parameters. + * + * @param resetErrorCount Whether the previous error count should be set to zero. + * @param retryDelayMillis The number of milliseconds to wait before retrying. + * @return A {@link LoadErrorAction} for retrying with the given parameters. + */ + public static LoadErrorAction createRetryAction(boolean resetErrorCount, long retryDelayMillis) { + return new LoadErrorAction( + resetErrorCount ? ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT : ACTION_TYPE_RETRY, + retryDelayMillis); + } + /** * Starts loading a {@link Loadable}. * @@ -250,15 +304,17 @@ public final class Loader implements LoaderErrorThrower { private static final int MSG_IO_EXCEPTION = 3; private static final int MSG_FATAL_ERROR = 4; - private final T loadable; - private final Loader.Callback callback; public final int defaultMinRetryCount; + + private final T loadable; private final long startTimeMs; + private @Nullable Loader.Callback callback; private IOException currentError; private int errorCount; private volatile Thread executorThread; + private volatile boolean canceled; private volatile boolean released; public LoadTask(Looper looper, T loadable, Loader.Callback callback, @@ -295,6 +351,7 @@ public final class Loader implements LoaderErrorThrower { sendEmptyMessage(MSG_CANCEL); } } else { + canceled = true; loadable.cancelLoad(); if (executorThread != null) { executorThread.interrupt(); @@ -304,6 +361,11 @@ public final class Loader implements LoaderErrorThrower { finish(); long nowMs = SystemClock.elapsedRealtime(); callback.onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true); + // If loading, this task will be referenced from a GC root (the loading thread) until + // cancellation completes. The time taken for cancellation to complete depends on the + // implementation of the Loadable that the task is loading. We null the callback reference + // here so that it doesn't prevent garbage collection whilst cancellation is ongoing. + callback = null; } } @@ -311,7 +373,7 @@ public final class Loader implements LoaderErrorThrower { public void run() { try { executorThread = Thread.currentThread(); - if (!loadable.isLoadCanceled()) { + if (!canceled) { TraceUtil.beginSection("load:" + loadable.getClass().getSimpleName()); try { loadable.load(); @@ -328,7 +390,7 @@ public final class Loader implements LoaderErrorThrower { } } catch (InterruptedException e) { // The load was canceled. - Assertions.checkState(loadable.isLoadCanceled()); + Assertions.checkState(canceled); if (!released) { sendEmptyMessage(MSG_END_OF_SOURCE); } @@ -373,7 +435,7 @@ public final class Loader implements LoaderErrorThrower { finish(); long nowMs = SystemClock.elapsedRealtime(); long durationMs = nowMs - startTimeMs; - if (loadable.isLoadCanceled()) { + if (canceled) { callback.onLoadCanceled(loadable, nowMs, durationMs, false); return; } @@ -392,12 +454,19 @@ public final class Loader implements LoaderErrorThrower { break; case MSG_IO_EXCEPTION: currentError = (IOException) msg.obj; - int retryAction = callback.onLoadError(loadable, nowMs, durationMs, currentError); - if (retryAction == DONT_RETRY_FATAL) { + errorCount++; + LoadErrorAction action = + callback.onLoadError(loadable, nowMs, durationMs, currentError, errorCount); + if (action.type == ACTION_TYPE_DONT_RETRY_FATAL) { fatalError = currentError; - } else if (retryAction != DONT_RETRY) { - errorCount = retryAction == RETRY_RESET_ERROR_COUNT ? 1 : errorCount + 1; - start(getRetryDelayMillis()); + } else if (action.type != ACTION_TYPE_DONT_RETRY) { + if (action.type == ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT) { + errorCount = 1; + } + start( + action.retryDelayMillis != C.TIME_UNSET + ? action.retryDelayMillis + : getRetryDelayMillis()); } break; default: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java index 7ef79b8963..17d479daab 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java @@ -16,9 +16,11 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.upstream.Loader.Loadable; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.io.InputStream; @@ -52,16 +54,17 @@ public final class ParsingLoadable implements Loadable { * Loads a single parsable object. * * @param dataSource The {@link DataSource} through which the object should be read. + * @param parser The {@link Parser} to parse the object from the response. * @param uri The {@link Uri} of the object to read. + * @param type The type of the data. One of the {@link C}{@code DATA_TYPE_*} constants. * @return The parsed object * @throws IOException Thrown if there is an error while loading or parsing. */ - public static T load(DataSource dataSource, Parser parser, Uri uri) + public static T load(DataSource dataSource, Parser parser, Uri uri, int type) throws IOException { - ParsingLoadable loadable = - new ParsingLoadable<>(dataSource, uri, C.DATA_TYPE_UNKNOWN, parser); + ParsingLoadable loadable = new ParsingLoadable<>(dataSource, uri, type, parser); loadable.load(); - return loadable.getResult(); + return Assertions.checkNotNull(loadable.getResult()); } /** @@ -74,12 +77,10 @@ public final class ParsingLoadable implements Loadable { */ public final int type; - private final DataSource dataSource; + private final StatsDataSource dataSource; private final Parser parser; - private volatile T result; - private volatile boolean isCanceled; - private volatile long bytesLoaded; + private volatile @Nullable T result; /** * @param dataSource A {@link DataSource} to use when loading the data. @@ -103,51 +104,50 @@ public final class ParsingLoadable implements Loadable { */ public ParsingLoadable(DataSource dataSource, DataSpec dataSpec, int type, Parser parser) { - this.dataSource = dataSource; + this.dataSource = new StatsDataSource(dataSource); this.dataSpec = dataSpec; this.type = type; this.parser = parser; } - /** - * Returns the loaded object, or null if an object has not been loaded. - */ - public final T getResult() { + /** Returns the loaded object, or null if an object has not been loaded. */ + public final @Nullable T getResult() { return result; } /** * Returns the number of bytes loaded. In the case that the network response was compressed, the - * value returned is the size of the data after decompression. - * - * @return The number of bytes loaded. + * value returned is the size of the data after decompression. Must only be called after + * the load completed, failed, or was canceled. */ public long bytesLoaded() { - return bytesLoaded; + return dataSource.getBytesRead(); + } + + /** + * Returns the {@link Uri} from which data was read. If redirection occurred, this is the + * redirected uri. Must only be called after the load completed, failed, or was canceled. + */ + public Uri getUri() { + return dataSource.getLastOpenedUri(); } @Override public final void cancelLoad() { - // We don't actually cancel anything, but we need to record the cancellation so that - // isLoadCanceled can return the correct value. - isCanceled = true; - } - - @Override - public final boolean isLoadCanceled() { - return isCanceled; + // Do nothing. } @Override public final void load() throws IOException { + // We always load from the beginning, so reset bytesRead to 0. + dataSource.resetBytesRead(); DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); try { inputStream.open(); - result = parser.parse(dataSource.getUri(), inputStream); + Uri dataSourceUri = Assertions.checkNotNull(dataSource.getUri()); + result = parser.parse(dataSourceUri, inputStream); } finally { - bytesLoaded = inputStream.bytesRead(); Util.closeQuietly(inputStream); } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java index 729f7fe179..9f9a3f9a91 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java @@ -20,6 +20,8 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.PriorityTaskManager; import java.io.IOException; +import java.util.List; +import java.util.Map; /** * A {@link DataSource} that can be used as part of a task registered with a @@ -51,6 +53,11 @@ public final class PriorityDataSource implements DataSource { this.priority = priority; } + @Override + public void addTransferListener(TransferListener transferListener) { + upstream.addTransferListener(transferListener); + } + @Override public long open(DataSpec dataSpec) throws IOException { priorityTaskManager.proceedOrThrow(priority); @@ -68,6 +75,11 @@ public final class PriorityDataSource implements DataSource { return upstream.getUri(); } + @Override + public Map> getResponseHeaders() { + return upstream.getResponseHeaders(); + } + @Override public void close() throws IOException { upstream.close(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java index 1b58c7e095..f86ed87c19 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java @@ -19,6 +19,7 @@ import android.content.Context; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.net.Uri; +import android.support.annotation.Nullable; import android.text.TextUtils; import com.google.android.exoplayer2.C; import java.io.EOFException; @@ -28,12 +29,12 @@ import java.io.InputStream; /** * A {@link DataSource} for reading a raw resource inside the APK. - *

    - * URIs supported by this source are of the form {@code rawresource:///rawResourceId}, where + * + *

    URIs supported by this source are of the form {@code rawresource:///rawResourceId}, where * rawResourceId is the integer identifier of a raw resource. {@link #buildRawResourceUri(int)} can * be used to build {@link Uri}s in this format. */ -public final class RawResourceDataSource implements DataSource { +public final class RawResourceDataSource extends BaseDataSource { /** * Thrown when an {@link IOException} is encountered reading from a raw resource. @@ -62,11 +63,10 @@ public final class RawResourceDataSource implements DataSource { public static final String RAW_RESOURCE_SCHEME = "rawresource"; private final Resources resources; - private final TransferListener listener; - private Uri uri; - private AssetFileDescriptor assetFileDescriptor; - private InputStream inputStream; + private @Nullable Uri uri; + private @Nullable AssetFileDescriptor assetFileDescriptor; + private @Nullable InputStream inputStream; private long bytesRemaining; private boolean opened; @@ -74,17 +74,22 @@ public final class RawResourceDataSource implements DataSource { * @param context A context. */ public RawResourceDataSource(Context context) { - this(context, null); + super(/* isNetwork= */ false); + this.resources = context.getResources(); } /** * @param context A context. * @param listener An optional listener. + * @deprecated Use {@link #RawResourceDataSource(Context)} and {@link + * #addTransferListener(TransferListener)}. */ - public RawResourceDataSource(Context context, - TransferListener listener) { - this.resources = context.getResources(); - this.listener = listener; + @Deprecated + public RawResourceDataSource(Context context, @Nullable TransferListener listener) { + this(context); + if (listener != null) { + addTransferListener(listener); + } } @Override @@ -102,6 +107,7 @@ public final class RawResourceDataSource implements DataSource { throw new RawResourceDataSourceException("Resource identifier must be an integer."); } + transferInitializing(dataSpec); assetFileDescriptor = resources.openRawResourceFd(resourceId); inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); inputStream.skip(assetFileDescriptor.getStartOffset()); @@ -124,9 +130,7 @@ public final class RawResourceDataSource implements DataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesRemaining; } @@ -158,14 +162,12 @@ public final class RawResourceDataSource implements DataSource { if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= bytesRead; } - if (listener != null) { - listener.onBytesTransferred(this, bytesRead); - } + bytesTransferred(bytesRead); return bytesRead; } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return uri; } @@ -190,9 +192,7 @@ public final class RawResourceDataSource implements DataSource { assetFileDescriptor = null; if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/StatsDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/StatsDataSource.java new file mode 100644 index 0000000000..04b29b531c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/StatsDataSource.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.upstream; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * {@link DataSource} wrapper which keeps track of bytes transferred, redirected uris, and response + * headers. + */ +public final class StatsDataSource implements DataSource { + + private final DataSource dataSource; + + private long bytesRead; + private Uri lastOpenedUri; + private Map> lastResponseHeaders; + + /** + * Creates the stats data source. + * + * @param dataSource The wrapped {@link DataSource}. + */ + public StatsDataSource(DataSource dataSource) { + this.dataSource = Assertions.checkNotNull(dataSource); + lastOpenedUri = Uri.EMPTY; + lastResponseHeaders = Collections.emptyMap(); + } + + /** Resets the number of bytes read as returned from {@link #getBytesRead()} to zero. */ + public void resetBytesRead() { + bytesRead = 0; + } + + /** Returns the total number of bytes that have been read from the data source. */ + public long getBytesRead() { + return bytesRead; + } + + /** + * Returns the {@link Uri} associated with the last {@link #open(DataSpec)} call. If redirection + * occurred, this is the redirected uri. + */ + public Uri getLastOpenedUri() { + return lastOpenedUri; + } + + /** Returns the response headers associated with the last {@link #open(DataSpec)} call. */ + public Map> getLastResponseHeaders() { + return lastResponseHeaders; + } + + @Override + public void addTransferListener(TransferListener transferListener) { + dataSource.addTransferListener(transferListener); + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + // Reassign defaults in case dataSource.open throws an exception. + lastOpenedUri = dataSpec.uri; + lastResponseHeaders = Collections.emptyMap(); + long availableBytes = dataSource.open(dataSpec); + lastOpenedUri = Assertions.checkNotNull(getUri()); + lastResponseHeaders = getResponseHeaders(); + return availableBytes; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + int bytesRead = dataSource.read(buffer, offset, readLength); + if (bytesRead != C.RESULT_END_OF_INPUT) { + this.bytesRead += bytesRead; + } + return bytesRead; + } + + @Override + public @Nullable Uri getUri() { + return dataSource.getUri(); + } + + @Override + public Map> getResponseHeaders() { + return dataSource.getResponseHeaders(); + } + + @Override + public void close() throws IOException { + dataSource.close(); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java index 6fcb08e6b5..3c021b0b74 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java @@ -16,9 +16,12 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.util.List; +import java.util.Map; /** * Tees data into a {@link DataSink} as the data is read. @@ -40,6 +43,11 @@ public final class TeeDataSource implements DataSource { this.dataSink = Assertions.checkNotNull(dataSink); } + @Override + public void addTransferListener(TransferListener transferListener) { + upstream.addTransferListener(transferListener); + } + @Override public long open(DataSpec dataSpec) throws IOException { bytesRemaining = upstream.open(dataSpec); @@ -48,14 +56,7 @@ public final class TeeDataSource implements DataSource { } if (dataSpec.length == C.LENGTH_UNSET && bytesRemaining != C.LENGTH_UNSET) { // Reconstruct dataSpec in order to provide the resolved length to the sink. - dataSpec = - new DataSpec( - dataSpec.uri, - dataSpec.absoluteStreamPosition, - dataSpec.position, - bytesRemaining, - dataSpec.key, - dataSpec.flags); + dataSpec = dataSpec.subrange(0, bytesRemaining); } dataSinkNeedsClosing = true; dataSink.open(dataSpec); @@ -79,10 +80,15 @@ public final class TeeDataSource implements DataSource { } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return upstream.getUri(); } + @Override + public Map> getResponseHeaders() { + return upstream.getResponseHeaders(); + } + @Override public void close() throws IOException { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/TransferListener.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/TransferListener.java index e061f0c7d0..a8971e71a4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/TransferListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/TransferListener.java @@ -17,31 +17,61 @@ package com.google.android.exoplayer2.upstream; /** * A listener of data transfer events. + * + *

    A transfer usually progresses through multiple steps: + * + *

      + *
    1. Initializing the underlying resource (e.g. opening a HTTP connection). {@link + * #onTransferInitializing(DataSource, DataSpec, boolean)} is called before the initialization + * starts. + *
    2. Starting the transfer after successfully initializing the resource. {@link + * #onTransferStart(DataSource, DataSpec, boolean)} is called. Note that this only happens if + * the initialization was successful. + *
    3. Transferring data. {@link #onBytesTransferred(DataSource, DataSpec, boolean, int)} is + * called frequently during the transfer to indicate progress. + *
    4. Closing the transfer and the underlying resource. {@link #onTransferEnd(DataSource, + * DataSpec, boolean)} is called. Note that each {@link #onTransferStart(DataSource, DataSpec, + * boolean)} will have exactly one corresponding call to {@link #onTransferEnd(DataSource, + * DataSpec, boolean)}. + *
    */ -public interface TransferListener { +public interface TransferListener { + + /** + * Called when a transfer is being initialized. + * + * @param source The source performing the transfer. + * @param dataSpec Describes the data for which the transfer is initialized. + * @param isNetwork Whether the data is transferred through a network. + */ + void onTransferInitializing(DataSource source, DataSpec dataSpec, boolean isNetwork); /** * Called when a transfer starts. * * @param source The source performing the transfer. * @param dataSpec Describes the data being transferred. + * @param isNetwork Whether the data is transferred through a network. */ - void onTransferStart(S source, DataSpec dataSpec); + void onTransferStart(DataSource source, DataSpec dataSpec, boolean isNetwork); /** * Called incrementally during a transfer. * * @param source The source performing the transfer. - * @param bytesTransferred The number of bytes transferred since the previous call to this - * method (or if the first call, since the transfer was started). + * @param dataSpec Describes the data being transferred. + * @param isNetwork Whether the data is transferred through a network. + * @param bytesTransferred The number of bytes transferred since the previous call to this method */ - void onBytesTransferred(S source, int bytesTransferred); + void onBytesTransferred( + DataSource source, DataSpec dataSpec, boolean isNetwork, int bytesTransferred); /** * Called when a transfer ends. * * @param source The source performing the transfer. + * @param dataSpec Describes the data being transferred. + * @param isNetwork Whether the data is transferred through a network. */ - void onTransferEnd(S source); - + void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java index 68a04d9182..47677d2c47 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.io.IOException; import java.net.DatagramPacket; @@ -25,10 +26,8 @@ import java.net.InetSocketAddress; import java.net.MulticastSocket; import java.net.SocketException; -/** - * A UDP {@link DataSource}. - */ -public final class UdpDataSource implements DataSource { +/** A UDP {@link DataSource}. */ +public final class UdpDataSource extends BaseDataSource { /** * Thrown when an error is encountered when trying to read from a {@link UdpDataSource}. @@ -46,60 +45,97 @@ public final class UdpDataSource implements DataSource { */ public static final int DEFAULT_MAX_PACKET_SIZE = 2000; - /** - * The default socket timeout, in milliseconds. - */ - public static final int DEAFULT_SOCKET_TIMEOUT_MILLIS = 8 * 1000; + /** The default socket timeout, in milliseconds. */ + public static final int DEFAULT_SOCKET_TIMEOUT_MILLIS = 8 * 1000; - private final TransferListener listener; private final int socketTimeoutMillis; private final byte[] packetBuffer; private final DatagramPacket packet; - private Uri uri; - private DatagramSocket socket; - private MulticastSocket multicastSocket; - private InetAddress address; - private InetSocketAddress socketAddress; + private @Nullable Uri uri; + private @Nullable DatagramSocket socket; + private @Nullable MulticastSocket multicastSocket; + private @Nullable InetAddress address; + private @Nullable InetSocketAddress socketAddress; private boolean opened; private int packetRemaining; - /** - * @param listener An optional listener. - */ - public UdpDataSource(TransferListener listener) { - this(listener, DEFAULT_MAX_PACKET_SIZE); + public UdpDataSource() { + this(DEFAULT_MAX_PACKET_SIZE); } /** - * @param listener An optional listener. + * Constructs a new instance. + * * @param maxPacketSize The maximum datagram packet size, in bytes. */ - public UdpDataSource(TransferListener listener, int maxPacketSize) { - this(listener, maxPacketSize, DEAFULT_SOCKET_TIMEOUT_MILLIS); + public UdpDataSource(int maxPacketSize) { + this(maxPacketSize, DEFAULT_SOCKET_TIMEOUT_MILLIS); } /** - * @param listener An optional listener. + * Constructs a new instance. + * * @param maxPacketSize The maximum datagram packet size, in bytes. * @param socketTimeoutMillis The socket timeout in milliseconds. A timeout of zero is interpreted * as an infinite timeout. */ - public UdpDataSource(TransferListener listener, int maxPacketSize, - int socketTimeoutMillis) { - this.listener = listener; + public UdpDataSource(int maxPacketSize, int socketTimeoutMillis) { + super(/* isNetwork= */ true); this.socketTimeoutMillis = socketTimeoutMillis; packetBuffer = new byte[maxPacketSize]; packet = new DatagramPacket(packetBuffer, 0, maxPacketSize); } + /** + * Constructs a new instance. + * + * @param listener An optional listener. + * @deprecated Use {@link #UdpDataSource()} and {@link #addTransferListener(TransferListener)}. + */ + @Deprecated + public UdpDataSource(@Nullable TransferListener listener) { + this(listener, DEFAULT_MAX_PACKET_SIZE); + } + + /** + * Constructs a new instance. + * + * @param listener An optional listener. + * @param maxPacketSize The maximum datagram packet size, in bytes. + * @deprecated Use {@link #UdpDataSource(int)} and {@link #addTransferListener(TransferListener)}. + */ + @Deprecated + public UdpDataSource(@Nullable TransferListener listener, int maxPacketSize) { + this(listener, maxPacketSize, DEFAULT_SOCKET_TIMEOUT_MILLIS); + } + + /** + * Constructs a new instance. + * + * @param listener An optional listener. + * @param maxPacketSize The maximum datagram packet size, in bytes. + * @param socketTimeoutMillis The socket timeout in milliseconds. A timeout of zero is interpreted + * as an infinite timeout. + * @deprecated Use {@link #UdpDataSource(int, int)} and {@link + * #addTransferListener(TransferListener)}. + */ + @Deprecated + public UdpDataSource( + @Nullable TransferListener listener, int maxPacketSize, int socketTimeoutMillis) { + this(maxPacketSize, socketTimeoutMillis); + if (listener != null) { + addTransferListener(listener); + } + } + @Override public long open(DataSpec dataSpec) throws UdpDataSourceException { uri = dataSpec.uri; String host = uri.getHost(); int port = uri.getPort(); - + transferInitializing(dataSpec); try { address = InetAddress.getByName(host); socketAddress = new InetSocketAddress(address, port); @@ -121,9 +157,7 @@ public final class UdpDataSource implements DataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return C.LENGTH_UNSET; } @@ -141,9 +175,7 @@ public final class UdpDataSource implements DataSource { throw new UdpDataSourceException(e); } packetRemaining = packet.getLength(); - if (listener != null) { - listener.onBytesTransferred(this, packetRemaining); - } + bytesTransferred(packetRemaining); } int packetOffset = packet.getLength() - packetRemaining; @@ -154,7 +186,7 @@ public final class UdpDataSource implements DataSource { } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return uri; } @@ -178,9 +210,7 @@ public final class UdpDataSource implements DataSource { packetRemaining = 0; if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 023567e7df..222d5385d3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -18,20 +18,23 @@ package com.google.android.exoplayer2.upstream.cache; import android.net.Uri; import android.support.annotation.IntDef; import android.support.annotation.Nullable; -import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod; import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.upstream.TeeDataSource; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.io.InterruptedIOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.Map; /** * A {@link DataSource} that reads and writes a {@link Cache}. Requests are fulfilled from the cache @@ -52,8 +55,6 @@ public final class CacheDataSource implements DataSource { */ public static final long DEFAULT_MAX_CACHE_FILE_SIZE = 2 * 1024 * 1024; - private static final String TAG = "CacheDataSource"; - /** * Flags controlling the cache's behavior. */ @@ -65,20 +66,20 @@ public final class CacheDataSource implements DataSource { * A flag indicating whether we will block reads if the cache key is locked. If unset then data is * read from upstream if the cache key is locked, regardless of whether the data is cached. */ - public static final int FLAG_BLOCK_ON_CACHE = 1 << 0; + public static final int FLAG_BLOCK_ON_CACHE = 1; /** * A flag indicating whether the cache is bypassed following any cache related error. If set * then cache related exceptions may be thrown for one cycle of open, read and close calls. * Subsequent cycles of these calls will then bypass the cache. */ - public static final int FLAG_IGNORE_CACHE_ON_ERROR = 1 << 1; + public static final int FLAG_IGNORE_CACHE_ON_ERROR = 1 << 1; // 2 /** * A flag indicating that the cache should be bypassed for requests whose lengths are unset. This * flag is provided for legacy reasons only. */ - public static final int FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS = 1 << 2; + public static final int FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS = 1 << 2; // 4 /** Reasons the cache may be ignored. */ @Retention(RetentionPolicy.SOURCE) @@ -120,23 +121,25 @@ public final class CacheDataSource implements DataSource { private final Cache cache; private final DataSource cacheReadDataSource; - private final DataSource cacheWriteDataSource; + private final @Nullable DataSource cacheWriteDataSource; private final DataSource upstreamDataSource; + private final CacheKeyFactory cacheKeyFactory; @Nullable private final EventListener eventListener; private final boolean blockOnCache; private final boolean ignoreCacheOnError; private final boolean ignoreCacheForUnsetLengthRequests; - private DataSource currentDataSource; + private @Nullable DataSource currentDataSource; private boolean currentDataSpecLengthUnset; - private Uri uri; - private Uri actualUri; + private @Nullable Uri uri; + private @Nullable Uri actualUri; + private @HttpMethod int httpMethod; private int flags; - private String key; + private @Nullable String key; private long readPosition; private long bytesRemaining; - private CacheSpan currentHoleSpan; + private @Nullable CacheSpan currentHoleSpan; private boolean seenCacheError; private boolean currentRequestIgnoresCache; private long totalCachedBytesRead; @@ -181,8 +184,13 @@ public final class CacheDataSource implements DataSource { */ public CacheDataSource(Cache cache, DataSource upstream, @Flags int flags, long maxCacheFileSize) { - this(cache, upstream, new FileDataSource(), new CacheDataSink(cache, maxCacheFileSize), - flags, null); + this( + cache, + upstream, + new FileDataSource(), + new CacheDataSink(cache, maxCacheFileSize), + flags, + /* eventListener= */ null); } /** @@ -201,8 +209,43 @@ public final class CacheDataSource implements DataSource { */ public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource, DataSink cacheWriteDataSink, @Flags int flags, @Nullable EventListener eventListener) { + this( + cache, + upstream, + cacheReadDataSource, + cacheWriteDataSink, + flags, + eventListener, + /* cacheKeyFactory= */ null); + } + + /** + * Constructs an instance with arbitrary {@link DataSource} and {@link DataSink} instances for + * reading and writing the cache. One use of this constructor is to allow data to be transformed + * before it is written to disk. + * + * @param cache The cache. + * @param upstream A {@link DataSource} for reading data not in the cache. + * @param cacheReadDataSource A {@link DataSource} for reading data from the cache. + * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is + * accessed read-only. + * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} + * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0. + * @param eventListener An optional {@link EventListener} to receive events. + * @param cacheKeyFactory An optional factory for cache keys. + */ + public CacheDataSource( + Cache cache, + DataSource upstream, + DataSource cacheReadDataSource, + DataSink cacheWriteDataSink, + @Flags int flags, + @Nullable EventListener eventListener, + @Nullable CacheKeyFactory cacheKeyFactory) { this.cache = cache; this.cacheReadDataSource = cacheReadDataSource; + this.cacheKeyFactory = + cacheKeyFactory != null ? cacheKeyFactory : CacheUtil.DEFAULT_CACHE_KEY_FACTORY; this.blockOnCache = (flags & FLAG_BLOCK_ON_CACHE) != 0; this.ignoreCacheOnError = (flags & FLAG_IGNORE_CACHE_ON_ERROR) != 0; this.ignoreCacheForUnsetLengthRequests = @@ -216,12 +259,19 @@ public final class CacheDataSource implements DataSource { this.eventListener = eventListener; } + @Override + public void addTransferListener(TransferListener transferListener) { + cacheReadDataSource.addTransferListener(transferListener); + upstreamDataSource.addTransferListener(transferListener); + } + @Override public long open(DataSpec dataSpec) throws IOException { try { - key = CacheUtil.getKey(dataSpec); + key = cacheKeyFactory.buildCacheKey(dataSpec); uri = dataSpec.uri; - actualUri = loadRedirectedUriOrReturnGivenUri(cache, key, uri); + actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri); + httpMethod = dataSpec.httpMethod; flags = dataSpec.flags; readPosition = dataSpec.position; @@ -272,7 +322,7 @@ public final class CacheDataSource implements DataSource { bytesRemaining -= bytesRead; } } else if (currentDataSpecLengthUnset) { - setBytesRemainingAndMaybeStoreLength(0); + setNoBytesRemainingAndMaybeStoreLength(); } else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) { closeCurrentSource(); openNextSource(false); @@ -281,7 +331,7 @@ public final class CacheDataSource implements DataSource { return bytesRead; } catch (IOException e) { if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) { - setBytesRemainingAndMaybeStoreLength(0); + setNoBytesRemainingAndMaybeStoreLength(); return C.RESULT_END_OF_INPUT; } handleBeforeThrow(e); @@ -290,14 +340,23 @@ public final class CacheDataSource implements DataSource { } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return actualUri; } + @Override + public Map> getResponseHeaders() { + // TODO: Implement. + return isReadingFromUpstream() + ? upstreamDataSource.getResponseHeaders() + : DataSource.super.getResponseHeaders(); + } + @Override public void close() throws IOException { uri = null; actualUri = null; + httpMethod = DataSpec.HTTP_METHOD_GET; notifyBytesRead(); try { closeCurrentSource(); @@ -342,7 +401,9 @@ public final class CacheDataSource implements DataSource { // The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read // from upstream. nextDataSource = upstreamDataSource; - nextDataSpec = new DataSpec(uri, readPosition, bytesRemaining, key, flags); + nextDataSpec = + new DataSpec( + uri, httpMethod, null, readPosition, readPosition, bytesRemaining, key, flags); } else if (nextSpan.isCached) { // Data is cached, read from cache. Uri fileUri = Uri.fromFile(nextSpan.file); @@ -364,7 +425,8 @@ public final class CacheDataSource implements DataSource { length = Math.min(length, bytesRemaining); } } - nextDataSpec = new DataSpec(uri, readPosition, length, key, flags); + nextDataSpec = + new DataSpec(uri, httpMethod, null, readPosition, readPosition, length, key, flags); if (cacheWriteDataSource != null) { nextDataSource = cacheWriteDataSource; } else { @@ -402,46 +464,38 @@ public final class CacheDataSource implements DataSource { currentDataSource = nextDataSource; currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET; long resolvedLength = nextDataSource.open(nextDataSpec); - if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) { - setBytesRemainingAndMaybeStoreLength(resolvedLength); - } - // TODO find a way to store length and redirected uri in one metadata mutation. - maybeUpdateActualUriFieldAndRedirectedUriMetadata(); - } - private void maybeUpdateActualUriFieldAndRedirectedUriMetadata() { - if (!isReadingFromUpstream()) { - return; - } - actualUri = currentDataSource.getUri(); - maybeUpdateRedirectedUriMetadata(); - } - - private void maybeUpdateRedirectedUriMetadata() { - if (!isWritingToCache()) { - return; - } + // Update bytesRemaining, actualUri and (if writing to cache) the cache metadata. ContentMetadataMutations mutations = new ContentMetadataMutations(); - boolean isRedirected = !uri.equals(actualUri); - if (isRedirected) { - ContentMetadataInternal.setRedirectedUri(mutations, actualUri); - } else { - ContentMetadataInternal.removeRedirectedUri(mutations); + if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) { + bytesRemaining = resolvedLength; + ContentMetadataInternal.setContentLength(mutations, readPosition + bytesRemaining); } - try { + if (isReadingFromUpstream()) { + actualUri = currentDataSource.getUri(); + boolean isRedirected = !uri.equals(actualUri); + if (isRedirected) { + ContentMetadataInternal.setRedirectedUri(mutations, actualUri); + } else { + ContentMetadataInternal.removeRedirectedUri(mutations); + } + } + if (isWritingToCache()) { cache.applyContentMetadataMutations(key, mutations); - } catch (CacheException e) { - String message = - "Couldn't update redirected URI. " - + "This might cause relative URIs get resolved incorrectly."; - Log.w(TAG, message, e); } } - private static Uri loadRedirectedUriOrReturnGivenUri(Cache cache, String key, Uri uri) { + private void setNoBytesRemainingAndMaybeStoreLength() throws IOException { + bytesRemaining = 0; + if (isWritingToCache()) { + cache.setContentLength(key, readPosition); + } + } + + private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) { ContentMetadata contentMetadata = cache.getContentMetadata(key); Uri redirectedUri = ContentMetadataInternal.getRedirectedUri(contentMetadata); - return redirectedUri == null ? uri : redirectedUri; + return redirectedUri == null ? defaultUri : redirectedUri; } private static boolean isCausedByPositionOutOfRange(IOException e) { @@ -458,13 +512,6 @@ public final class CacheDataSource implements DataSource { return false; } - private void setBytesRemainingAndMaybeStoreLength(long bytesRemaining) throws IOException { - this.bytesRemaining = bytesRemaining; - if (isWritingToCache()) { - cache.setContentLength(key, readPosition + bytesRemaining); - } - } - private boolean isReadingFromUpstream() { return !isReadingFromCache(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderException.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java similarity index 50% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderException.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java index 433a656982..bfa404c074 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,26 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.metadata; +package com.google.android.exoplayer2.upstream.cache; -/** - * Thrown when an error occurs decoding metadata. - */ -public class MetadataDecoderException extends Exception { +import com.google.android.exoplayer2.upstream.DataSpec; + +/** Factory for cache keys. */ +public interface CacheKeyFactory { /** - * @param message The detail message for this exception. + * Returns a cache key for the given {@link DataSpec}. + * + * @param dataSpec The data being cached. */ - public MetadataDecoderException(String message) { - super(message); - } - - /** - * @param message The detail message for this exception. - * @param cause The cause of this exception. - */ - public MetadataDecoderException(String message, Throwable cause) { - super(message, cause); - } - + String buildCacheKey(DataSpec dataSpec); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java index a1f7aa3097..1bdaa8e3fa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java @@ -54,6 +54,15 @@ public final class CacheUtil { /** Default buffer size to be used while caching. */ public static final int DEFAULT_BUFFER_SIZE_BYTES = 128 * 1024; + /** Default {@link CacheKeyFactory} that calls through to {@link #getKey}. */ + public static final CacheKeyFactory DEFAULT_CACHE_KEY_FACTORY = + new CacheKeyFactory() { + @Override + public String buildCacheKey(DataSpec dataSpec) { + return getKey(dataSpec); + } + }; + /** * Generates a cache key out of the given {@link Uri}. * @@ -129,11 +138,11 @@ public final class CacheUtil { cache, new CacheDataSource(cache, upstream), new byte[DEFAULT_BUFFER_SIZE_BYTES], - null, - 0, + /* priorityTaskManager= */ null, + /* priority= */ 0, counters, - null, - false); + isCanceled, + /* enableEOFException= */ false); } /** @@ -186,9 +195,7 @@ public final class CacheUtil { long start = dataSpec.absoluteStreamPosition; long left = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : cache.getContentLength(key); while (left != 0) { - if (isCanceled != null && isCanceled.get()) { - throw new InterruptedException(); - } + throwExceptionIfInterruptedOrCancelled(isCanceled); long blockLength = cache.getCachedLength(key, start, left != C.LENGTH_UNSET ? left : Long.MAX_VALUE); if (blockLength > 0) { @@ -196,8 +203,17 @@ public final class CacheUtil { } else { // There is a hole in the cache which is at least "-blockLength" long. blockLength = -blockLength; - long read = readAndDiscard(dataSpec, start, blockLength, dataSource, buffer, - priorityTaskManager, priority, counters); + long read = + readAndDiscard( + dataSpec, + start, + blockLength, + dataSource, + buffer, + priorityTaskManager, + priority, + counters, + isCanceled); if (read < blockLength) { // Reached to the end of the data. if (enableEOFException && left != C.LENGTH_UNSET) { @@ -224,36 +240,47 @@ public final class CacheUtil { * caching. * @param priority The priority of this task. * @param counters Counters to be set during reading. + * @param isCanceled An optional flag that will interrupt caching if set to true. * @return Number of read bytes, or 0 if no data is available because the end of the opened range * has been reached. */ - private static long readAndDiscard(DataSpec dataSpec, long absoluteStreamPosition, long length, - DataSource dataSource, byte[] buffer, PriorityTaskManager priorityTaskManager, int priority, - CachingCounters counters) throws IOException, InterruptedException { + private static long readAndDiscard( + DataSpec dataSpec, + long absoluteStreamPosition, + long length, + DataSource dataSource, + byte[] buffer, + PriorityTaskManager priorityTaskManager, + int priority, + CachingCounters counters, + AtomicBoolean isCanceled) + throws IOException, InterruptedException { while (true) { if (priorityTaskManager != null) { // Wait for any other thread with higher priority to finish its job. priorityTaskManager.proceed(priority); } try { - if (Thread.interrupted()) { - throw new InterruptedException(); - } + throwExceptionIfInterruptedOrCancelled(isCanceled); // Create a new dataSpec setting length to C.LENGTH_UNSET to prevent getting an error in // case the given length exceeds the end of input. - dataSpec = new DataSpec(dataSpec.uri, dataSpec.postBody, absoluteStreamPosition, - dataSpec.position + absoluteStreamPosition - dataSpec.absoluteStreamPosition, - C.LENGTH_UNSET, dataSpec.key, - dataSpec.flags | DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH); + dataSpec = + new DataSpec( + dataSpec.uri, + dataSpec.httpMethod, + dataSpec.httpBody, + absoluteStreamPosition, + dataSpec.position + absoluteStreamPosition - dataSpec.absoluteStreamPosition, + C.LENGTH_UNSET, + dataSpec.key, + dataSpec.flags | DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH); long resolvedLength = dataSource.open(dataSpec); if (counters.contentLength == C.LENGTH_UNSET && resolvedLength != C.LENGTH_UNSET) { counters.contentLength = dataSpec.absoluteStreamPosition + resolvedLength; } long totalRead = 0; while (totalRead != length) { - if (Thread.interrupted()) { - throw new InterruptedException(); - } + throwExceptionIfInterruptedOrCancelled(isCanceled); int read = dataSource.read(buffer, 0, length != C.LENGTH_UNSET ? (int) Math.min(buffer.length, length - totalRead) : buffer.length); @@ -287,6 +314,13 @@ public final class CacheUtil { } } + private static void throwExceptionIfInterruptedOrCancelled(AtomicBoolean isCanceled) + throws InterruptedException { + if (Thread.interrupted() || (isCanceled != null && isCanceled.get())) { + throw new InterruptedException(); + } + } + private CacheUtil() {} } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataInternal.java index 3376dd6944..0065018260 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataInternal.java @@ -20,7 +20,7 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; /** Helper classes to easily access and modify internal metadata values. */ -/*package*/ final class ContentMetadataInternal { +/* package */ final class ContentMetadataInternal { private static final String PREFIX = ContentMetadata.INTERNAL_METADATA_NAME_PREFIX; private static final String METADATA_NAME_REDIRECTED_URI = PREFIX + "redir"; @@ -59,4 +59,8 @@ import com.google.android.exoplayer2.C; public static void removeRedirectedUri(ContentMetadataMutations mutations) { mutations.remove(METADATA_NAME_REDIRECTED_URI); } + + private ContentMetadataInternal() { + // Prevent instantiation. + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java index aefb0f6852..cf63bcc4f6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java @@ -34,7 +34,7 @@ public final class DefaultContentMetadata implements ContentMetadata { /** An empty DefaultContentMetadata. */ public static final DefaultContentMetadata EMPTY = - new DefaultContentMetadata(Collections.emptyMap()); + new DefaultContentMetadata(Collections.emptyMap()); private static final int MAX_VALUE_LENGTH = 10 * 1024 * 1024; private int hashCode; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java index 26ac3b38fa..801c84dc51 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java @@ -16,10 +16,14 @@ package com.google.android.exoplayer2.upstream.crypto; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.TransferListener; import java.io.IOException; +import java.util.List; +import java.util.Map; import javax.crypto.Cipher; /** @@ -30,13 +34,18 @@ public final class AesCipherDataSource implements DataSource { private final DataSource upstream; private final byte[] secretKey; - private AesFlushingCipher cipher; + private @Nullable AesFlushingCipher cipher; public AesCipherDataSource(byte[] secretKey, DataSource upstream) { this.upstream = upstream; this.secretKey = secretKey; } + @Override + public void addTransferListener(TransferListener transferListener) { + upstream.addTransferListener(transferListener); + } + @Override public long open(DataSpec dataSpec) throws IOException { long dataLength = upstream.open(dataSpec); @@ -59,15 +68,19 @@ public final class AesCipherDataSource implements DataSource { return read; } + @Override + public @Nullable Uri getUri() { + return upstream.getUri(); + } + + @Override + public Map> getResponseHeaders() { + return upstream.getResponseHeaders(); + } + @Override public void close() throws IOException { cipher = null; upstream.close(); } - - @Override - public Uri getUri() { - return upstream.getUri(); - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java index e093eb3064..1721b1d8b7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream.crypto; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; @@ -49,7 +50,9 @@ public final class AesFlushingCipher { flushedBlock = new byte[blockSize]; long counter = offset / blockSize; int startPadding = (int) (offset % blockSize); - cipher.init(mode, new SecretKeySpec(secretKey, cipher.getAlgorithm().split("/")[0]), + cipher.init( + mode, + new SecretKeySpec(secretKey, Util.splitAtFirst(cipher.getAlgorithm(), "/")[0]), new IvParameterSpec(getInitializationVector(nonce, counter))); if (startPadding != 0) { updateInPlace(new byte[startPadding], 0, startPadding); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java index 53c196a14f..c6ad5dfe52 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java @@ -104,6 +104,7 @@ public final class Assertions { * @return The non-null reference that was validated. * @throws NullPointerException If {@code reference} is null. */ + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) @EnsuresNonNull({"#1"}) public static T checkNotNull(@Nullable T reference) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { @@ -122,6 +123,7 @@ public final class Assertions { * @return The non-null reference that was validated. * @throws NullPointerException If {@code reference} is null. */ + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) @EnsuresNonNull({"#1"}) public static T checkNotNull(@Nullable T reference, Object errorMessage) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { @@ -137,6 +139,7 @@ public final class Assertions { * @return The non-null, non-empty string that was validated. * @throws IllegalArgumentException If {@code string} is null or 0-length. */ + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) @EnsuresNonNull({"#1"}) public static String checkNotEmpty(@Nullable String string) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) { @@ -154,6 +157,7 @@ public final class Assertions { * @return The non-null, non-empty string that was validated. * @throws IllegalArgumentException If {@code string} is null or 0-length. */ + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) @EnsuresNonNull({"#1"}) public static String checkNotEmpty(@Nullable String string, Object errorMessage) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java index 0514d9dbdc..627cf7e070 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.util; +import android.support.annotation.Nullable; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; @@ -204,6 +205,21 @@ public final class CodecSpecificDataUtil { return specificConfig; } + /** + * Builds an RFC 6381 AVC codec string using the provided parameters. + * + * @param profileIdc The encoding profile. + * @param constraintsFlagsAndReservedZero2Bits The constraint flags followed by the reserved zero + * 2 bits, all contained in the least significant byte of the integer. + * @param levelIdc The encoding level. + * @return An RFC 6381 AVC codec string built using the provided parameters. + */ + public static String buildAvcCodecString( + int profileIdc, int constraintsFlagsAndReservedZero2Bits, int levelIdc) { + return String.format( + "avc1.%02X%02X%02X", profileIdc, constraintsFlagsAndReservedZero2Bits, levelIdc); + } + /** * Constructs a NAL unit consisting of the NAL start code followed by the specified data. * @@ -221,8 +237,8 @@ public final class CodecSpecificDataUtil { /** * Splits an array of NAL units. - *

    - * If the input consists of NAL start code delimited units, then the returned array consists of + * + *

    If the input consists of NAL start code delimited units, then the returned array consists of * the split NAL units, each of which is still prefixed with the NAL start code. For any other * input, null is returned. * @@ -230,7 +246,7 @@ public final class CodecSpecificDataUtil { * @return The individual NAL units, or null if the input did not consist of NAL start code * delimited units. */ - public static byte[][] splitNalUnits(byte[] data) { + public static @Nullable byte[][] splitNalUnits(byte[] data) { if (!isNalStartCode(data, 0)) { // data does not consist of NAL start code delimited units. return null; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java index a9df80e9fe..54f52e0a14 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java @@ -26,7 +26,7 @@ import java.util.regex.Pattern; * * @see WebVTT CSS Styling * @see Timed Text Markup Language 2 (TTML2) - 10.3.5 - **/ + */ public final class ColorParser { private static final String RGB = "rgb"; @@ -271,4 +271,7 @@ public final class ColorParser { COLOR_MAP.put("yellowgreen", 0xFF9ACD32); } + private ColorParser() { + // Prevent instantiation. + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java index 6fe76b9b2c..7e831f0512 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java @@ -33,6 +33,12 @@ import java.lang.annotation.RetentionPolicy; @TargetApi(17) public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableListener, Runnable { + /** Listener to be called when the texture image on {@link SurfaceTexture} has been updated. */ + public interface TextureImageListener { + /** Called when the {@link SurfaceTexture} receives a new frame from its image producer. */ + void onFrameAvailable(); + } + /** Secure mode to be used by the EGL surface and context. */ @Retention(RetentionPolicy.SOURCE) @IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER}) @@ -45,6 +51,9 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL /** Creating a secure surface backed by a pixel buffer. */ public static final int SECURE_MODE_PROTECTED_PBUFFER = 2; + private static final int EGL_SURFACE_WIDTH = 1; + private static final int EGL_SURFACE_HEIGHT = 1; + private static final int[] EGL_CONFIG_ATTRIBUTES = new int[] { EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, @@ -69,6 +78,7 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL private final Handler handler; private final int[] textureIdHolder; + private final @Nullable TextureImageListener callback; private @Nullable EGLDisplay display; private @Nullable EGLContext context; @@ -82,7 +92,21 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL * looper. */ public EGLSurfaceTexture(Handler handler) { + this(handler, /* callback= */ null); + } + + /** + * @param handler The {@link Handler} that will be used to call {@link + * SurfaceTexture#updateTexImage()} to update images on the {@link SurfaceTexture}. Note that + * {@link #init(int)} has to be called on the same looper thread as the looper of the {@link + * Handler}. + * @param callback The {@link TextureImageListener} to be called when the texture image on {@link + * SurfaceTexture} has been updated. This callback will be called on the same handler thread + * as the {@code handler}. + */ + public EGLSurfaceTexture(Handler handler, @Nullable TextureImageListener callback) { this.handler = handler; + this.callback = callback; textureIdHolder = new int[1]; } @@ -111,12 +135,25 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL GLES20.glDeleteTextures(1, textureIdHolder, 0); } } finally { + if (display != null && !display.equals(EGL14.EGL_NO_DISPLAY)) { + EGL14.eglMakeCurrent( + display, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); + } if (surface != null && !surface.equals(EGL14.EGL_NO_SURFACE)) { EGL14.eglDestroySurface(display, surface); } if (context != null) { EGL14.eglDestroyContext(display, context); } + // EGL14.eglReleaseThread could crash before Android K (see [internal: b/11327779]). + if (Util.SDK_INT >= 19) { + EGL14.eglReleaseThread(); + } + if (display != null && !display.equals(EGL14.EGL_NO_DISPLAY)) { + // Android is unusual in that it uses a reference-counted EGLDisplay. So for + // every eglInitialize() we need an eglTerminate(). + EGL14.eglTerminate(display); + } display = null; context = null; surface = null; @@ -142,8 +179,20 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL @Override public void run() { + // Run on the provided handler thread when a new image frame is available. + dispatchOnFrameAvailable(); if (texture != null) { - texture.updateTexImage(); + try { + texture.updateTexImage(); + } catch (RuntimeException e) { + // Ignore + } + } + } + + private void dispatchOnFrameAvailable() { + if (callback != null) { + callback.onFrameAvailable(); } } @@ -220,9 +269,9 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL pbufferAttributes = new int[] { EGL14.EGL_WIDTH, - 1, + EGL_SURFACE_WIDTH, EGL14.EGL_HEIGHT, - 1, + EGL_SURFACE_HEIGHT, EGL_PROTECTED_CONTENT_EXT, EGL14.EGL_TRUE, EGL14.EGL_NONE @@ -230,8 +279,10 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL } else { pbufferAttributes = new int[] { - EGL14.EGL_WIDTH, 1, - EGL14.EGL_HEIGHT, 1, + EGL14.EGL_WIDTH, + EGL_SURFACE_WIDTH, + EGL14.EGL_HEIGHT, + EGL_SURFACE_HEIGHT, EGL14.EGL_NONE }; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EventDispatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventDispatcher.java new file mode 100644 index 0000000000..26c02d8ae9 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventDispatcher.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.util; + +import android.os.Handler; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Event dispatcher which allows listener registration. + * + * @param The type of listener. + */ +public final class EventDispatcher { + + /** Functional interface to send an event. */ + public interface Event { + + /** + * Sends the event to a listener. + * + * @param listener The listener to send the event to. + */ + void sendTo(T listener); + } + + /** The list of listeners and handlers. */ + private final CopyOnWriteArrayList> listeners; + + /** Creates event dispatcher. */ + public EventDispatcher() { + listeners = new CopyOnWriteArrayList<>(); + } + + /** Adds listener to event dispatcher. */ + public void addListener(Handler handler, T eventListener) { + Assertions.checkArgument(handler != null && eventListener != null); + removeListener(eventListener); + listeners.add(new HandlerAndListener<>(handler, eventListener)); + } + + /** Removes listener from event dispatcher. */ + public void removeListener(T eventListener) { + for (HandlerAndListener handlerAndListener : listeners) { + if (handlerAndListener.listener == eventListener) { + listeners.remove(handlerAndListener); + } + } + } + + /** + * Dispatches an event to all registered listeners. + * + * @param event The {@link Event}. + */ + public void dispatch(Event event) { + for (HandlerAndListener handlerAndListener : listeners) { + T eventListener = handlerAndListener.listener; + handlerAndListener.handler.post(() -> event.sendTo(eventListener)); + } + } + + private static final class HandlerAndListener { + + public final Handler handler; + public final T listener; + + public HandlerAndListener(Handler handler, T eventListener) { + this.handler = handler; + this.listener = eventListener; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index deb09f8074..3ca463e5e4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.util; -import android.net.NetworkInfo; import android.os.SystemClock; import android.support.annotation.Nullable; import android.util.Log; @@ -364,13 +363,8 @@ public class EventLogger implements AnalyticsListener { } @Override - public void onViewportSizeChange(EventTime eventTime, int width, int height) { - logd(eventTime, "viewportSizeChanged", width + ", " + height); - } - - @Override - public void onNetworkTypeChanged(EventTime eventTime, @Nullable NetworkInfo networkInfo) { - logd(eventTime, "networkTypeChanged", networkInfo == null ? "none" : networkInfo.toString()); + public void onSurfaceSizeChanged(EventTime eventTime, int width, int height) { + logd(eventTime, "surfaceSizeChanged", width + ", " + height); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index d13aa877e0..e0b1df7739 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.util; import android.support.annotation.Nullable; import android.text.TextUtils; import com.google.android.exoplayer2.C; +import java.util.ArrayList; /** * Defines common MIME types and helper methods. @@ -92,7 +93,29 @@ public final class MimeTypes { public static final String APPLICATION_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs"; public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif"; - private MimeTypes() {} + private static final ArrayList customMimeTypes = new ArrayList<>(); + + /** + * Registers a custom MIME type. Most applications do not need to call this method, as handling of + * standard MIME types is built in. These built-in MIME types take precedence over any registered + * via this method. If this method is used, it must be called before creating any player(s). + * + * @param mimeType The custom MIME type to register. + * @param codecPrefix The RFC 6381-style codec string prefix associated with the MIME type. + * @param trackType The {@link C}{@code .TRACK_TYPE_*} constant associated with the MIME type. + * This value is ignored if the top-level type of {@code mimeType} is audio, video or text. + */ + public static void registerCustomMimeType(String mimeType, String codecPrefix, int trackType) { + CustomMimeType customMimeType = new CustomMimeType(mimeType, codecPrefix, trackType); + int customMimeTypeCount = customMimeTypes.size(); + for (int i = 0; i < customMimeTypeCount; i++) { + if (mimeType.equals(customMimeTypes.get(i).mimeType)) { + customMimeTypes.remove(i); + break; + } + } + customMimeTypes.add(customMimeType); + } /** * Whether the top-level type of {@code mimeType} is audio. @@ -100,7 +123,7 @@ public final class MimeTypes { * @param mimeType The mimeType to test. * @return Whether the top level type is audio. */ - public static boolean isAudio(String mimeType) { + public static boolean isAudio(@Nullable String mimeType) { return BASE_TYPE_AUDIO.equals(getTopLevelType(mimeType)); } @@ -110,7 +133,7 @@ public final class MimeTypes { * @param mimeType The mimeType to test. * @return Whether the top level type is video. */ - public static boolean isVideo(String mimeType) { + public static boolean isVideo(@Nullable String mimeType) { return BASE_TYPE_VIDEO.equals(getTopLevelType(mimeType)); } @@ -120,7 +143,7 @@ public final class MimeTypes { * @param mimeType The mimeType to test. * @return Whether the top level type is text. */ - public static boolean isText(String mimeType) { + public static boolean isText(@Nullable String mimeType) { return BASE_TYPE_TEXT.equals(getTopLevelType(mimeType)); } @@ -130,7 +153,7 @@ public final class MimeTypes { * @param mimeType The mimeType to test. * @return Whether the top level type is application. */ - public static boolean isApplication(String mimeType) { + public static boolean isApplication(@Nullable String mimeType) { return BASE_TYPE_APPLICATION.equals(getTopLevelType(mimeType)); } @@ -144,7 +167,7 @@ public final class MimeTypes { if (codecs == null) { return null; } - String[] codecList = codecs.split(","); + String[] codecList = Util.splitCodecs(codecs); for (String codec : codecList) { String mimeType = getMediaMimeType(codec); if (mimeType != null && isVideo(mimeType)) { @@ -164,7 +187,7 @@ public final class MimeTypes { if (codecs == null) { return null; } - String[] codecList = codecs.split(","); + String[] codecList = Util.splitCodecs(codecs); for (String codec : codecList) { String mimeType = getMediaMimeType(codec); if (mimeType != null && isAudio(mimeType)) { @@ -222,13 +245,14 @@ public final class MimeTypes { return MimeTypes.AUDIO_OPUS; } else if (codec.startsWith("vorbis")) { return MimeTypes.AUDIO_VORBIS; + } else { + return getCustomMimeTypeForCodec(codec); } - return null; } /** * Derives a mimeType from MP4 object type identifier, as defined in RFC 6381 and - * http://www.mp4ra.org/object.html. + * https://mp4ra.org/#/object_types. * * @param objectType The objectType identifier to derive. * @return The mimeType, or null if it could not be derived. @@ -236,18 +260,28 @@ public final class MimeTypes { @Nullable public static String getMimeTypeFromMp4ObjectType(int objectType) { switch (objectType) { - case 0x60: - case 0x61: - return MimeTypes.VIDEO_MPEG2; case 0x20: return MimeTypes.VIDEO_MP4V; case 0x21: return MimeTypes.VIDEO_H264; case 0x23: return MimeTypes.VIDEO_H265; + case 0x60: + case 0x61: + case 0x62: + case 0x63: + case 0x64: + case 0x65: + return MimeTypes.VIDEO_MPEG2; + case 0x6A: + return MimeTypes.VIDEO_MPEG; case 0x69: case 0x6B: return MimeTypes.AUDIO_MPEG; + case 0xA3: + return MimeTypes.VIDEO_VC1; + case 0xB1: + return MimeTypes.VIDEO_VP9; case 0x40: case 0x66: case 0x67: @@ -278,7 +312,7 @@ public final class MimeTypes { * @param mimeType The MIME type. * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified MIME type. */ - public static int getTrackType(String mimeType) { + public static int getTrackType(@Nullable String mimeType) { if (TextUtils.isEmpty(mimeType)) { return C.TRACK_TYPE_UNKNOWN; } else if (isAudio(mimeType)) { @@ -298,7 +332,7 @@ public final class MimeTypes { || APPLICATION_CAMERA_MOTION.equals(mimeType)) { return C.TRACK_TYPE_METADATA; } else { - return C.TRACK_TYPE_UNKNOWN; + return getTrackTypeForCustomMimeType(mimeType); } } @@ -355,4 +389,41 @@ public final class MimeTypes { return mimeType.substring(0, indexOfSlash); } + private static @Nullable String getCustomMimeTypeForCodec(String codec) { + int customMimeTypeCount = customMimeTypes.size(); + for (int i = 0; i < customMimeTypeCount; i++) { + CustomMimeType customMimeType = customMimeTypes.get(i); + if (codec.startsWith(customMimeType.codecPrefix)) { + return customMimeType.mimeType; + } + } + return null; + } + + private static int getTrackTypeForCustomMimeType(String mimeType) { + int customMimeTypeCount = customMimeTypes.size(); + for (int i = 0; i < customMimeTypeCount; i++) { + CustomMimeType customMimeType = customMimeTypes.get(i); + if (mimeType.equals(customMimeType.mimeType)) { + return customMimeType.trackType; + } + } + return C.TRACK_TYPE_UNKNOWN; + } + + private MimeTypes() { + // Prevent instantiation. + } + + private static final class CustomMimeType { + public final String mimeType; + public final String codecPrefix; + public final int trackType; + + public CustomMimeType(String mimeType, String codecPrefix, int trackType) { + this.mimeType = mimeType; + this.codecPrefix = codecPrefix; + this.trackType = trackType; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java index c4ed20546d..b3fbe43194 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java @@ -31,6 +31,9 @@ public final class NalUnitUtil { */ public static final class SpsData { + public final int profileIdc; + public final int constraintsFlagsAndReservedZero2Bits; + public final int levelIdc; public final int seqParameterSetId; public final int width; public final int height; @@ -42,9 +45,23 @@ public final class NalUnitUtil { public final int picOrderCntLsbLength; public final boolean deltaPicOrderAlwaysZeroFlag; - public SpsData(int seqParameterSetId, int width, int height, float pixelWidthAspectRatio, - boolean separateColorPlaneFlag, boolean frameMbsOnlyFlag, int frameNumLength, - int picOrderCountType, int picOrderCntLsbLength, boolean deltaPicOrderAlwaysZeroFlag) { + public SpsData( + int profileIdc, + int constraintsFlagsAndReservedZero2Bits, + int levelIdc, + int seqParameterSetId, + int width, + int height, + float pixelWidthAspectRatio, + boolean separateColorPlaneFlag, + boolean frameMbsOnlyFlag, + int frameNumLength, + int picOrderCountType, + int picOrderCntLsbLength, + boolean deltaPicOrderAlwaysZeroFlag) { + this.profileIdc = profileIdc; + this.constraintsFlagsAndReservedZero2Bits = constraintsFlagsAndReservedZero2Bits; + this.levelIdc = levelIdc; this.seqParameterSetId = seqParameterSetId; this.width = width; this.height = height; @@ -251,7 +268,8 @@ public final class NalUnitUtil { ParsableNalUnitBitArray data = new ParsableNalUnitBitArray(nalData, nalOffset, nalLimit); data.skipBits(8); // nal_unit int profileIdc = data.readBits(8); - data.skipBits(16); // constraint bits (6), reserved (2) and level_idc (8) + int constraintsFlagsAndReservedZero2Bits = data.readBits(8); + int levelIdc = data.readBits(8); int seqParameterSetId = data.readUnsignedExpGolombCodedInt(); int chromaFormatIdc = 1; // Default is 4:2:0 @@ -349,9 +367,20 @@ public final class NalUnitUtil { } } - return new SpsData(seqParameterSetId, frameWidth, frameHeight, pixelWidthHeightRatio, - separateColorPlaneFlag, frameMbsOnlyFlag, frameNumLength, picOrderCntType, - picOrderCntLsbLength, deltaPicOrderAlwaysZeroFlag); + return new SpsData( + profileIdc, + constraintsFlagsAndReservedZero2Bits, + levelIdc, + seqParameterSetId, + frameWidth, + frameHeight, + pixelWidthHeightRatio, + separateColorPlaneFlag, + frameMbsOnlyFlag, + frameNumLength, + picOrderCntType, + picOrderCntLsbLength, + deltaPicOrderAlwaysZeroFlag); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java index fb5f9525e9..c60caf9ba8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java @@ -175,7 +175,7 @@ public final class ParsableBitArray { bitOffset -= 8; returnValue |= (data[byteOffset++] & 0xFF) << bitOffset; } - returnValue |= (data[byteOffset] & 0xFF) >> 8 - bitOffset; + returnValue |= (data[byteOffset] & 0xFF) >> (8 - bitOffset); returnValue &= 0xFFFFFFFF >>> (32 - numBits); if (bitOffset == 8) { bitOffset = 0; @@ -199,17 +199,18 @@ public final class ParsableBitArray { int to = offset + (numBits >> 3) /* numBits / 8 */; for (int i = offset; i < to; i++) { buffer[i] = (byte) (data[byteOffset++] << bitOffset); - buffer[i] |= (data[byteOffset] & 0xFF) >> (8 - bitOffset); + buffer[i] = (byte) (buffer[i] | ((data[byteOffset] & 0xFF) >> (8 - bitOffset))); } // Trailing bits. int bitsLeft = numBits & 7 /* numBits % 8 */; if (bitsLeft == 0) { return; } - buffer[to] &= 0xFF >> bitsLeft; // Set to 0 the bits that are going to be overwritten. + // Set bits that are going to be overwritten to 0. + buffer[to] = (byte) (buffer[to] & (0xFF >> bitsLeft)); if (bitOffset + bitsLeft > 8) { // We read the rest of data[byteOffset] and increase byteOffset. - buffer[to] |= (byte) ((data[byteOffset++] & 0xFF) << bitOffset); + buffer[to] = (byte) (buffer[to] | ((data[byteOffset++] & 0xFF) << bitOffset)); bitOffset -= 8; } bitOffset += bitsLeft; @@ -280,9 +281,10 @@ public final class ParsableBitArray { int firstByteReadSize = Math.min(8 - bitOffset, numBits); int firstByteRightPaddingSize = 8 - bitOffset - firstByteReadSize; int firstByteBitmask = (0xFF00 >> bitOffset) | ((1 << firstByteRightPaddingSize) - 1); - data[byteOffset] &= firstByteBitmask; + data[byteOffset] = (byte) (data[byteOffset] & firstByteBitmask); int firstByteInputBits = value >>> (numBits - firstByteReadSize); - data[byteOffset] |= firstByteInputBits << firstByteRightPaddingSize; + data[byteOffset] = + (byte) (data[byteOffset] | (firstByteInputBits << firstByteRightPaddingSize)); remainingBitsToRead -= firstByteReadSize; int currentByteIndex = byteOffset + 1; while (remainingBitsToRead > 8) { @@ -290,9 +292,11 @@ public final class ParsableBitArray { remainingBitsToRead -= 8; } int lastByteRightPaddingSize = 8 - remainingBitsToRead; - data[currentByteIndex] &= (1 << lastByteRightPaddingSize) - 1; + data[currentByteIndex] = + (byte) (data[currentByteIndex] & ((1 << lastByteRightPaddingSize) - 1)); int lastByteInput = value & ((1 << remainingBitsToRead) - 1); - data[currentByteIndex] |= lastByteInput << lastByteRightPaddingSize; + data[currentByteIndex] = + (byte) (data[currentByteIndex] | (lastByteInput << lastByteRightPaddingSize)); skipBits(numBits); assertValidOffset(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index 57313ea895..5190896d9f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -470,7 +470,7 @@ public final class ParsableByteArray { if (lastIndex < limit && data[lastIndex] == 0) { stringLength--; } - String result = new String(data, position, stringLength); + String result = Util.fromUtf8Bytes(data, position, stringLength); position += length; return result; } @@ -489,7 +489,7 @@ public final class ParsableByteArray { while (stringLimit < limit && data[stringLimit] != 0) { stringLimit++; } - String string = new String(data, position, stringLimit - position); + String string = Util.fromUtf8Bytes(data, position, stringLimit - position); position = stringLimit; if (position < limit) { position++; @@ -520,7 +520,7 @@ public final class ParsableByteArray { // There's a byte order mark at the start of the line. Discard it. position += 3; } - String line = new String(data, position, lineLimit - position); + String line = Util.fromUtf8Bytes(data, position, lineLimit - position); position = lineLimit; if (position == limit) { return line; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java index 443c69909c..3a7202c674 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java @@ -140,7 +140,7 @@ public final class ParsableNalUnitBitArray { returnValue |= (data[byteOffset] & 0xFF) << bitOffset; byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1; } - returnValue |= (data[byteOffset] & 0xFF) >> 8 - bitOffset; + returnValue |= (data[byteOffset] & 0xFF) >> (8 - bitOffset); returnValue &= 0xFFFFFFFF >>> (32 - numBits); if (bitOffset == 8) { bitOffset = 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Predicate.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Predicate.java index 889b19f3c3..b582cf3f7c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Predicate.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Predicate.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer2.util; /** - * Determines a true of false value for a given input. + * Determines a true or false value for a given input. * * @param The input type of the predicate. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java index 53cb051230..d386206bdd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 The Android Open Source Project + * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,10 +40,8 @@ public final class RepeatModeUtil { * "Repeat One" button enabled. */ public static final int REPEAT_TOGGLE_MODE_ONE = 1; - /** - * "Repeat All" button enabled. - */ - public static final int REPEAT_TOGGLE_MODE_ALL = 2; + /** "Repeat All" button enabled. */ + public static final int REPEAT_TOGGLE_MODE_ALL = 1 << 1; // 2 private RepeatModeUtil() { // Prevent instantiation. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java b/library/core/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java index 08e2bd0669..439374a086 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java @@ -30,7 +30,8 @@ public final class TimestampAdjuster { public static final long DO_NOT_OFFSET = Long.MAX_VALUE; /** - * The value one greater than the largest representable (33 bit) MPEG-2 TS presentation timestamp. + * The value one greater than the largest representable (33 bit) MPEG-2 TS 90 kHz clock + * presentation timestamp. */ private static final long MAX_PTS_PLUS_ONE = 0x200000000L; @@ -38,13 +39,13 @@ public final class TimestampAdjuster { private long timestampOffsetUs; // Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp. - private volatile long lastSampleTimestamp; + private volatile long lastSampleTimestampUs; /** * @param firstSampleTimestampUs See {@link #setFirstSampleTimestampUs(long)}. */ public TimestampAdjuster(long firstSampleTimestampUs) { - lastSampleTimestamp = C.TIME_UNSET; + lastSampleTimestampUs = C.TIME_UNSET; setFirstSampleTimestampUs(firstSampleTimestampUs); } @@ -56,30 +57,24 @@ public final class TimestampAdjuster { * {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset. */ public synchronized void setFirstSampleTimestampUs(long firstSampleTimestampUs) { - Assertions.checkState(lastSampleTimestamp == C.TIME_UNSET); + Assertions.checkState(lastSampleTimestampUs == C.TIME_UNSET); this.firstSampleTimestampUs = firstSampleTimestampUs; } - /** - * Returns the first adjusted sample timestamp in microseconds. - * - * @return The first adjusted sample timestamp in microseconds. - */ + /** Returns the last value passed to {@link #setFirstSampleTimestampUs(long)}. */ public long getFirstSampleTimestampUs() { return firstSampleTimestampUs; } /** - * Returns the last adjusted timestamp. If no timestamp has been adjusted, returns - * {@code firstSampleTimestampUs} as provided to the constructor. If this value is - * {@link #DO_NOT_OFFSET}, returns {@link C#TIME_UNSET}. - * - * @return The last adjusted timestamp. If not present, {@code firstSampleTimestampUs} is - * returned unless equal to {@link #DO_NOT_OFFSET}, in which case {@link C#TIME_UNSET} is - * returned. + * Returns the last value obtained from {@link #adjustSampleTimestamp}. If {@link + * #adjustSampleTimestamp} has not been called, returns the result of calling {@link + * #getFirstSampleTimestampUs()}. If this value is {@link #DO_NOT_OFFSET}, returns {@link + * C#TIME_UNSET}. */ public long getLastAdjustedTimestampUs() { - return lastSampleTimestamp != C.TIME_UNSET ? lastSampleTimestamp + return lastSampleTimestampUs != C.TIME_UNSET + ? (lastSampleTimestampUs + timestampOffsetUs) : firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET; } @@ -93,44 +88,47 @@ public final class TimestampAdjuster { * be offset. */ public long getTimestampOffsetUs() { - return firstSampleTimestampUs == DO_NOT_OFFSET ? 0 - : lastSampleTimestamp == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs; + return firstSampleTimestampUs == DO_NOT_OFFSET + ? 0 + : lastSampleTimestampUs == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs; } /** * Resets the instance to its initial state. */ public void reset() { - lastSampleTimestamp = C.TIME_UNSET; + lastSampleTimestampUs = C.TIME_UNSET; } /** * Scales and offsets an MPEG-2 TS presentation timestamp considering wraparound. * - * @param pts The MPEG-2 TS presentation timestamp. + * @param pts90Khz A 90 kHz clock MPEG-2 TS presentation timestamp. * @return The adjusted timestamp in microseconds. */ - public long adjustTsTimestamp(long pts) { - if (pts == C.TIME_UNSET) { + public long adjustTsTimestamp(long pts90Khz) { + if (pts90Khz == C.TIME_UNSET) { return C.TIME_UNSET; } - if (lastSampleTimestamp != C.TIME_UNSET) { + if (lastSampleTimestampUs != C.TIME_UNSET) { // The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1), - // and we need to snap to the one closest to lastSampleTimestamp. - long lastPts = usToPts(lastSampleTimestamp); + // and we need to snap to the one closest to lastSampleTimestampUs. + long lastPts = usToPts(lastSampleTimestampUs); long closestWrapCount = (lastPts + (MAX_PTS_PLUS_ONE / 2)) / MAX_PTS_PLUS_ONE; - long ptsWrapBelow = pts + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1)); - long ptsWrapAbove = pts + (MAX_PTS_PLUS_ONE * closestWrapCount); - pts = Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts) - ? ptsWrapBelow : ptsWrapAbove; + long ptsWrapBelow = pts90Khz + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1)); + long ptsWrapAbove = pts90Khz + (MAX_PTS_PLUS_ONE * closestWrapCount); + pts90Khz = + Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts) + ? ptsWrapBelow + : ptsWrapAbove; } - return adjustSampleTimestamp(ptsToUs(pts)); + return adjustSampleTimestamp(ptsToUs(pts90Khz)); } /** - * Offsets a sample timestamp in microseconds. + * Offsets a timestamp in microseconds. * - * @param timeUs The timestamp of a sample to adjust. + * @param timeUs The timestamp to adjust in microseconds. * @return The adjusted timestamp in microseconds. */ public long adjustSampleTimestamp(long timeUs) { @@ -138,15 +136,15 @@ public final class TimestampAdjuster { return C.TIME_UNSET; } // Record the adjusted PTS to adjust for wraparound next time. - if (lastSampleTimestamp != C.TIME_UNSET) { - lastSampleTimestamp = timeUs; + if (lastSampleTimestampUs != C.TIME_UNSET) { + lastSampleTimestampUs = timeUs; } else { if (firstSampleTimestampUs != DO_NOT_OFFSET) { // Calculate the timestamp offset. timestampOffsetUs = firstSampleTimestampUs - timeUs; } synchronized (this) { - lastSampleTimestamp = timeUs; + lastSampleTimestampUs = timeUs; // Notify threads waiting for this adjuster to be initialized. notifyAll(); } @@ -160,15 +158,15 @@ public final class TimestampAdjuster { * @throws InterruptedException If the thread was interrupted. */ public synchronized void waitUntilInitialized() throws InterruptedException { - while (lastSampleTimestamp == C.TIME_UNSET) { + while (lastSampleTimestampUs == C.TIME_UNSET) { wait(); } } /** - * Converts a value in MPEG-2 timestamp units to the corresponding value in microseconds. + * Converts a 90 kHz clock timestamp to a timestamp in microseconds. * - * @param pts A value in MPEG-2 timestamp units. + * @param pts A 90 kHz clock timestamp. * @return The corresponding value in microseconds. */ public static long ptsToUs(long pts) { @@ -176,10 +174,10 @@ public final class TimestampAdjuster { } /** - * Converts a value in microseconds to the corresponding values in MPEG-2 timestamp units. + * Converts a timestamp in microseconds to a 90 kHz clock timestamp. * * @param us A value in microseconds. - * @return The corresponding value in MPEG-2 timestamp units. + * @return The corresponding value as a 90 kHz clock timestamp. */ public static long usToPts(long us) { return (us * 90000) / C.MICROS_PER_SECOND; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index fe83ce13e6..58a4f64816 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -25,11 +25,18 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.Point; +import android.media.AudioFormat; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; +import android.os.Handler; +import android.os.Looper; import android.os.Parcel; +import android.security.NetworkSecurityPolicy; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; import android.view.Display; @@ -63,6 +70,12 @@ import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.PolyNull; /** * Miscellaneous utility methods. @@ -173,6 +186,29 @@ public final class Util { return false; } + /** + * Returns whether it may be possible to load the given URIs based on the network security + * policy's cleartext traffic permissions. + * + * @param uris A list of URIs that will be loaded. + * @return Whether it may be possible to load the given URIs. + */ + @TargetApi(24) + public static boolean checkCleartextTrafficPermitted(Uri... uris) { + if (Util.SDK_INT < 24) { + // We assume cleartext traffic is permitted. + return true; + } + for (Uri uri : uris) { + if ("http".equals(uri.getScheme()) + && !NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(uri.getHost())) { + // The security policy prevents cleartext traffic. + return false; + } + } + return true; + } + /** * Returns true if the URI is a path to a local file or a reference to a local file. * @@ -225,6 +261,24 @@ public final class Util { list.subList(fromIndex, toIndex).clear(); } + /** + * Casts a nullable variable to a non-null variable without runtime null check. + * + *

    Use {@link Assertions#checkNotNull(Object)} to throw if the value is null. + */ + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) + @EnsuresNonNull("#1") + public static T castNonNull(@Nullable T value) { + return value; + } + + /** Casts a nullable type array to a non-null type array without runtime null check. */ + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) + @EnsuresNonNull("#1") + public static T[] castNonNullTypeArray(@NullableType T[] value) { + return value; + } + /** * Copies and optionally truncates an array. Prevents null array elements created by {@link * Arrays#copyOf(Object[], int)} by ensuring the new length does not exceed the current length. @@ -233,12 +287,53 @@ public final class Util { * @param length The output array length. Must be less or equal to the length of the input array. * @return The copied array. */ - @SuppressWarnings("nullness:assignment.type.incompatible") + @SuppressWarnings({"nullness:argument.type.incompatible", "nullness:return.type.incompatible"}) public static T[] nullSafeArrayCopy(T[] input, int length) { Assertions.checkArgument(length <= input.length); return Arrays.copyOf(input, length); } + /** + * Creates a {@link Handler} with the specified {@link Handler.Callback} on the current {@link + * Looper} thread. The method accepts partially initialized objects as callback under the + * assumption that the Handler won't be used to send messages until the callback is fully + * initialized. + * + *

    If the current thread doesn't have a {@link Looper}, the application's main thread {@link + * Looper} is used. + * + * @param callback A {@link Handler.Callback}. May be a partially initialized class. + * @return A {@link Handler} with the specified callback on the current {@link Looper} thread. + */ + public static Handler createHandler(Handler.@UnknownInitialization Callback callback) { + return createHandler(getLooper(), callback); + } + + /** + * Creates a {@link Handler} with the specified {@link Handler.Callback} on the specified {@link + * Looper} thread. The method accepts partially initialized objects as callback under the + * assumption that the Handler won't be used to send messages until the callback is fully + * initialized. + * + * @param looper A {@link Looper} to run the callback on. + * @param callback A {@link Handler.Callback}. May be a partially initialized class. + * @return A {@link Handler} with the specified callback on the current {@link Looper} thread. + */ + @SuppressWarnings({"nullness:argument.type.incompatible", "nullness:return.type.incompatible"}) + public static Handler createHandler( + Looper looper, Handler.@UnknownInitialization Callback callback) { + return new Handler(looper, callback); + } + + /** + * Returns the {@link Looper} associated with the current thread, or the {@link Looper} of the + * application's main thread if the current thread doesn't have a {@link Looper}. + */ + public static Looper getLooper() { + Looper myLooper = Looper.myLooper(); + return myLooper != null ? myLooper : Looper.getMainLooper(); + } + /** * Instantiates a new single threaded executor whose thread has the specified name. * @@ -311,10 +406,10 @@ public final class Util { * Returns a normalized RFC 639-2/T code for {@code language}. * * @param language A case-insensitive ISO 639 alpha-2 or alpha-3 language code. - * @return The all-lowercase normalized code, or null if the input was null, or - * {@code language.toLowerCase()} if the language could not be normalized. + * @return The all-lowercase normalized code, or null if the input was null, or {@code + * language.toLowerCase()} if the language could not be normalized. */ - public static String normalizeLanguageCode(String language) { + public static @Nullable String normalizeLanguageCode(@Nullable String language) { try { return language == null ? null : new Locale(language).getISO3Language(); } catch (MissingResourceException e) { @@ -332,6 +427,18 @@ public final class Util { return new String(bytes, Charset.forName(C.UTF8_NAME)); } + /** + * Returns a new {@link String} constructed by decoding UTF-8 encoded bytes in a subarray. + * + * @param bytes The UTF-8 encoded bytes to decode. + * @param offset The index of the first byte to decode. + * @param length The number of bytes to decode. + * @return The string. + */ + public static String fromUtf8Bytes(byte[] bytes, int offset, int length) { + return new String(bytes, offset, length, Charset.forName(C.UTF8_NAME)); + } + /** * Returns a new byte array containing the code points of a {@link String} encoded using UTF-8. * @@ -342,6 +449,33 @@ public final class Util { return value.getBytes(Charset.forName(C.UTF8_NAME)); } + /** + * Splits a string using {@code value.split(regex, -1}). Note: this is is similar to {@link + * String#split(String)} but empty matches at the end of the string will not be omitted from the + * returned array. + * + * @param value The string to split. + * @param regex A delimiting regular expression. + * @return The array of strings resulting from splitting the string. + */ + public static String[] split(String value, String regex) { + return value.split(regex, /* limit= */ -1); + } + + /** + * Splits the string at the first occurrence of the delimiter {@code regex}. If the delimiter does + * not match, returns an array with one element which is the input string. If the delimiter does + * match, returns an array with the portion of the string before the delimiter and the rest of the + * string. + * + * @param value The string. + * @param regex A delimiting regular expression. + * @return The string split by the first occurrence of the delimiter. + */ + public static String[] splitAtFirst(String value, String regex) { + return value.split(regex, /* limit= */ 2); + } + /** * Returns whether the given character is a carriage return ('\r') or a line feed ('\n'). * @@ -358,8 +492,8 @@ public final class Util { * @param text The text to convert. * @return The lower case text, or null if {@code text} is null. */ - public static String toLowerInvariant(String text) { - return text == null ? null : text.toLowerCase(Locale.US); + public static @PolyNull String toLowerInvariant(@PolyNull String text) { + return text == null ? text : text.toLowerCase(Locale.US); } /** @@ -368,8 +502,8 @@ public final class Util { * @param text The text to convert. * @return The upper case text, or null if {@code text} is null. */ - public static String toUpperInvariant(String text) { - return text == null ? null : text.toUpperCase(Locale.US); + public static @PolyNull String toUpperInvariant(@PolyNull String text) { + return text == null ? text : text.toUpperCase(Locale.US); } /** @@ -540,10 +674,10 @@ public final class Util { /** * Returns the index of the largest element in {@code list} that is less than (or optionally equal * to) a specified {@code value}. - *

    - * The search is performed using a binary search algorithm, so the list must be sorted. If the - * list contains multiple elements equal to {@code value} and {@code inclusive} is true, the - * index of the first one will be returned. + * + *

    The search is performed using a binary search algorithm, so the list must be sorted. If the + * list contains multiple elements equal to {@code value} and {@code inclusive} is true, the index + * of the first one will be returned. * * @param The type of values being searched. * @param list The list to search. @@ -556,8 +690,11 @@ public final class Util { * @return The index of the largest element in {@code list} that is less than (or optionally equal * to) {@code value}. */ - public static int binarySearchFloor(List> list, T value, - boolean inclusive, boolean stayInBounds) { + public static > int binarySearchFloor( + List> list, + T value, + boolean inclusive, + boolean stayInBounds) { int index = Collections.binarySearch(list, value); if (index < 0) { index = -(index + 2); @@ -606,10 +743,10 @@ public final class Util { /** * Returns the index of the smallest element in {@code list} that is greater than (or optionally * equal to) a specified value. - *

    - * The search is performed using a binary search algorithm, so the list must be sorted. If the - * list contains multiple elements equal to {@code value} and {@code inclusive} is true, the - * index of the last one will be returned. + * + *

    The search is performed using a binary search algorithm, so the list must be sorted. If the + * list contains multiple elements equal to {@code value} and {@code inclusive} is true, the index + * of the last one will be returned. * * @param The type of values being searched. * @param list The list to search. @@ -618,13 +755,16 @@ public final class Util { * index. If false then the returned index corresponds to the smallest element strictly * greater than the value. * @param stayInBounds If true, then {@code (list.size() - 1)} will be returned in the case that - * the value is greater than the largest element in the list. If false then - * {@code list.size()} will be returned. + * the value is greater than the largest element in the list. If false then {@code + * list.size()} will be returned. * @return The index of the smallest element in {@code list} that is greater than (or optionally * equal to) {@code value}. */ - public static int binarySearchCeil(List> list, T value, - boolean inclusive, boolean stayInBounds) { + public static > int binarySearchCeil( + List> list, + T value, + boolean inclusive, + boolean stayInBounds) { int index = Collections.binarySearch(list, value); if (index < 0) { index = ~index; @@ -883,7 +1023,7 @@ public final class Util { * @param list A list of integers. * @return The list in array form, or null if the input list was null. */ - public static int[] toArray(List list) { + public static int @PolyNull [] toArray(@PolyNull List list) { if (list == null) { return null; } @@ -966,19 +1106,19 @@ public final class Util { } /** - * Returns a copy of {@code codecs} without the codecs whose track type doesn't match - * {@code trackType}. + * Returns a copy of {@code codecs} without the codecs whose track type doesn't match {@code + * trackType}. * * @param codecs A codec sequence string, as defined in RFC 6381. * @param trackType One of {@link C}{@code .TRACK_TYPE_*}. - * @return A copy of {@code codecs} without the codecs whose track type doesn't match - * {@code trackType}. + * @return A copy of {@code codecs} without the codecs whose track type doesn't match {@code + * trackType}. */ - public static String getCodecsOfType(String codecs, int trackType) { - if (TextUtils.isEmpty(codecs)) { + public static @Nullable String getCodecsOfType(String codecs, int trackType) { + String[] codecArray = splitCodecs(codecs); + if (codecArray.length == 0) { return null; } - String[] codecArray = codecs.trim().split("(\\s*,\\s*)"); StringBuilder builder = new StringBuilder(); for (String codec : codecArray) { if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) { @@ -991,6 +1131,19 @@ public final class Util { return builder.length() > 0 ? builder.toString() : null; } + /** + * Splits a codecs sequence string, as defined in RFC 6381, into individual codec strings. + * + * @param codecs A codec sequence string, as defined in RFC 6381. + * @return The split codecs, or an array of length zero if the input was empty. + */ + public static String[] splitCodecs(String codecs) { + if (TextUtils.isEmpty(codecs)) { + return new String[0]; + } + return split(codecs.trim(), "(\\s*,\\s*)"); + } + /** * Converts a sample bit depth to a corresponding PCM encoding constant. * @@ -1017,12 +1170,12 @@ public final class Util { } /** - * Returns whether {@code encoding} is one of the PCM encodings. + * Returns whether {@code encoding} is one of the linear PCM encodings. * * @param encoding The encoding of the audio data. * @return Whether the encoding is one of the PCM encodings. */ - public static boolean isEncodingPcm(@C.Encoding int encoding) { + public static boolean isEncodingLinearPcm(@C.Encoding int encoding) { return encoding == C.ENCODING_PCM_8BIT || encoding == C.ENCODING_PCM_16BIT || encoding == C.ENCODING_PCM_24BIT @@ -1040,6 +1193,47 @@ public final class Util { return encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT; } + /** + * Returns the audio track channel configuration for the given channel count, or {@link + * AudioFormat#CHANNEL_INVALID} if output is not poossible. + * + * @param channelCount The number of channels in the input audio. + * @return The channel configuration or {@link AudioFormat#CHANNEL_INVALID} if output is not + * possible. + */ + public static int getAudioTrackChannelConfig(int channelCount) { + switch (channelCount) { + case 1: + return AudioFormat.CHANNEL_OUT_MONO; + case 2: + return AudioFormat.CHANNEL_OUT_STEREO; + case 3: + return AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER; + case 4: + return AudioFormat.CHANNEL_OUT_QUAD; + case 5: + return AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER; + case 6: + return AudioFormat.CHANNEL_OUT_5POINT1; + case 7: + return AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER; + case 8: + if (Util.SDK_INT >= 23) { + return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; + } else if (Util.SDK_INT >= 21) { + // Equal to AudioFormat.CHANNEL_OUT_7POINT1_SURROUND, which is hidden before Android M. + return AudioFormat.CHANNEL_OUT_5POINT1 + | AudioFormat.CHANNEL_OUT_SIDE_LEFT + | AudioFormat.CHANNEL_OUT_SIDE_RIGHT; + } else { + // 8 ch output is not supported before Android L. + return AudioFormat.CHANNEL_INVALID; + } + default: + return AudioFormat.CHANNEL_INVALID; + } + } + /** * Returns the frame size for audio with {@code channelCount} channels in the specified encoding. * @@ -1151,7 +1345,7 @@ public final class Util { * "clearkey"}. * @return The derived {@link UUID}, or {@code null} if one could not be derived. */ - public static UUID getDrmUuid(String drmScheme) { + public static @Nullable UUID getDrmUuid(String drmScheme) { switch (Util.toLowerInvariant(drmScheme)) { case "widevine": return C.WIDEVINE_UUID; @@ -1327,7 +1521,7 @@ public final class Util { * @return The original value of the file name before it was escaped, or null if the escaped * fileName seems invalid. */ - public static String unescapeFileName(String fileName) { + public static @Nullable String unescapeFileName(String fileName) { int length = fileName.length(); int percentCharacterCount = 0; for (int i = 0; i < length; i++) { @@ -1373,8 +1567,9 @@ public final class Util { /** Recursively deletes a directory and its content. */ public static void recursiveDelete(File fileOrDirectory) { - if (fileOrDirectory.isDirectory()) { - for (File child : fileOrDirectory.listFiles()) { + File[] directoryFiles = fileOrDirectory.listFiles(); + if (directoryFiles != null) { + for (File child : directoryFiles) { recursiveDelete(child); } } @@ -1412,6 +1607,119 @@ public final class Util { return initialValue; } + /** + * Returns the {@link C.NetworkType} of the current network connection. {@link + * C#NETWORK_TYPE_UNKNOWN} will be returned if the {@code ACCESS_NETWORK_STATE} permission is not + * granted or the network connection type couldn't be determined. + * + * @param context A context to access the connectivity manager. + * @return The {@link C.NetworkType} of the current network connection, or {@link + * C#NETWORK_TYPE_UNKNOWN} if the {@code ACCESS_NETWORK_STATE} permission is not granted or + * {@code context} is null. + */ + public static @C.NetworkType int getNetworkType(@Nullable Context context) { + if (context == null) { + return C.NETWORK_TYPE_UNKNOWN; + } + NetworkInfo networkInfo; + try { + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivityManager == null) { + return C.NETWORK_TYPE_UNKNOWN; + } + networkInfo = connectivityManager.getActiveNetworkInfo(); + } catch (SecurityException e) { + // Permission ACCESS_NETWORK_STATE not granted. + return C.NETWORK_TYPE_UNKNOWN; + } + if (networkInfo == null || !networkInfo.isConnected()) { + return C.NETWORK_TYPE_OFFLINE; + } + switch (networkInfo.getType()) { + case ConnectivityManager.TYPE_WIFI: + return C.NETWORK_TYPE_WIFI; + case ConnectivityManager.TYPE_WIMAX: + return C.NETWORK_TYPE_4G; + case ConnectivityManager.TYPE_MOBILE: + case ConnectivityManager.TYPE_MOBILE_DUN: + case ConnectivityManager.TYPE_MOBILE_HIPRI: + return getMobileNetworkType(networkInfo); + case ConnectivityManager.TYPE_ETHERNET: + return C.NETWORK_TYPE_ETHERNET; + default: // Ethernet, VPN, Bluetooth, Dummy. + return C.NETWORK_TYPE_OTHER; + } + } + + /** + * Returns the upper-case ISO 3166-1 alpha-2 country code of the current registered operator's MCC + * (Mobile Country Code), or the country code of the default Locale if not available. + * + * @param context A context to access the telephony service. If null, only the Locale can be used. + * @return The upper-case ISO 3166-1 alpha-2 country code, or an empty String if unavailable. + */ + public static String getCountryCode(@Nullable Context context) { + if (context != null) { + TelephonyManager telephonyManager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + if (telephonyManager != null) { + String countryCode = telephonyManager.getNetworkCountryIso(); + if (!TextUtils.isEmpty(countryCode)) { + return toUpperInvariant(countryCode); + } + } + } + return toUpperInvariant(Locale.getDefault().getCountry()); + } + + /** + * Uncompresses the data in {@code input}. + * + * @param input Wraps the compressed input data. + * @param output Wraps an output buffer to be used to store the uncompressed data. If {@code + * output.data} is null or it isn't big enough to hold the uncompressed data, a new array is + * created. If {@code true} is returned then the output's position will be set to 0 and its + * limit will be set to the length of the uncompressed data. + * @param inflater If not null, used to uncompressed the input. Otherwise a new {@link Inflater} + * is created. + * @return Whether the input is uncompressed successfully. + */ + public static boolean inflate( + ParsableByteArray input, ParsableByteArray output, @Nullable Inflater inflater) { + if (input.bytesLeft() <= 0) { + return false; + } + byte[] outputData = output.data; + if (outputData == null) { + outputData = new byte[input.bytesLeft()]; + } + if (inflater == null) { + inflater = new Inflater(); + } + inflater.setInput(input.data, input.getPosition(), input.bytesLeft()); + try { + int outputSize = 0; + while (true) { + outputSize += inflater.inflate(outputData, outputSize, outputData.length - outputSize); + if (inflater.finished()) { + output.reset(outputData, outputSize); + return true; + } + if (inflater.needsDictionary() || inflater.needsInput()) { + return false; + } + if (outputSize == outputData.length) { + outputData = Arrays.copyOf(outputData, outputData.length * 2); + } + } + } catch (DataFormatException e) { + return false; + } finally { + inflater.reset(); + } + } + /** * Gets the physical size of the default display, in pixels. * @@ -1454,7 +1762,7 @@ public final class Util { // If we managed to read sys.display-size, attempt to parse it. if (!TextUtils.isEmpty(sysDisplaySize)) { try { - String[] sysDisplaySizeParts = sysDisplaySize.trim().split("x"); + String[] sysDisplaySizeParts = split(sysDisplaySize.trim(), "x"); if (sysDisplaySizeParts.length == 2) { int width = Integer.parseInt(sysDisplaySizeParts[0]); int height = Integer.parseInt(sysDisplaySizeParts[1]); @@ -1506,6 +1814,36 @@ public final class Util { outSize.y = display.getHeight(); } + private static @C.NetworkType int getMobileNetworkType(NetworkInfo networkInfo) { + switch (networkInfo.getSubtype()) { + case TelephonyManager.NETWORK_TYPE_EDGE: + case TelephonyManager.NETWORK_TYPE_GPRS: + return C.NETWORK_TYPE_2G; + case TelephonyManager.NETWORK_TYPE_1xRTT: + case TelephonyManager.NETWORK_TYPE_CDMA: + case TelephonyManager.NETWORK_TYPE_EVDO_0: + case TelephonyManager.NETWORK_TYPE_EVDO_A: + case TelephonyManager.NETWORK_TYPE_EVDO_B: + case TelephonyManager.NETWORK_TYPE_HSDPA: + case TelephonyManager.NETWORK_TYPE_HSPA: + case TelephonyManager.NETWORK_TYPE_HSUPA: + case TelephonyManager.NETWORK_TYPE_IDEN: + case TelephonyManager.NETWORK_TYPE_UMTS: + case TelephonyManager.NETWORK_TYPE_EHRPD: + case TelephonyManager.NETWORK_TYPE_HSPAP: + case TelephonyManager.NETWORK_TYPE_TD_SCDMA: + return C.NETWORK_TYPE_3G; + case TelephonyManager.NETWORK_TYPE_LTE: + return C.NETWORK_TYPE_4G; + case TelephonyManager.NETWORK_TYPE_IWLAN: + return C.NETWORK_TYPE_WIFI; + case TelephonyManager.NETWORK_TYPE_GSM: + case TelephonyManager.NETWORK_TYPE_UNKNOWN: + default: // Future mobile network types. + return C.NETWORK_TYPE_CELLULAR_UNKNOWN; + } + } + /** * Allows the CRC calculation to be done byte by byte instead of bit per bit being the order * "most significant bit first". diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/XmlPullParserUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/XmlPullParserUtil.java index 84a6e4cebf..6d568b14c6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/XmlPullParserUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/XmlPullParserUtil.java @@ -56,8 +56,7 @@ public final class XmlPullParserUtil { * @return Whether the current event is a start tag with the specified name. * @throws XmlPullParserException If an error occurs querying the parser. */ - public static boolean isStartTag(XmlPullParser xpp, String name) - throws XmlPullParserException { + public static boolean isStartTag(XmlPullParser xpp, String name) throws XmlPullParserException { return isStartTag(xpp) && xpp.getName().equals(name); } @@ -72,22 +71,59 @@ public final class XmlPullParserUtil { return xpp.getEventType() == XmlPullParser.START_TAG; } + /** + * Returns whether the current event is a start tag with the specified name. If the current event + * has a raw name then its prefix is stripped before matching. + * + * @param xpp The {@link XmlPullParser} to query. + * @param name The specified name. + * @return Whether the current event is a start tag with the specified name. + * @throws XmlPullParserException If an error occurs querying the parser. + */ + public static boolean isStartTagIgnorePrefix(XmlPullParser xpp, String name) + throws XmlPullParserException { + return isStartTag(xpp) && stripPrefix(xpp.getName()).equals(name); + } + /** * Returns the value of an attribute of the current start tag. * * @param xpp The {@link XmlPullParser} to query. * @param attributeName The name of the attribute. * @return The value of the attribute, or null if the current event is not a start tag or if no - * no such attribute was found. + * such attribute was found. */ public static String getAttributeValue(XmlPullParser xpp, String attributeName) { int attributeCount = xpp.getAttributeCount(); for (int i = 0; i < attributeCount; i++) { - if (attributeName.equals(xpp.getAttributeName(i))) { + if (xpp.getAttributeName(i).equals(attributeName)) { return xpp.getAttributeValue(i); } } return null; } + /** + * Returns the value of an attribute of the current start tag. Any raw attribute names in the + * current start tag have their prefixes stripped before matching. + * + * @param xpp The {@link XmlPullParser} to query. + * @param attributeName The name of the attribute. + * @return The value of the attribute, or null if the current event is not a start tag or if no + * such attribute was found. + */ + public static String getAttributeValueIgnorePrefix(XmlPullParser xpp, String attributeName) { + int attributeCount = xpp.getAttributeCount(); + for (int i = 0; i < attributeCount; i++) { + if (stripPrefix(xpp.getAttributeName(i)).equals(attributeName)) { + return xpp.getAttributeValue(i); + } + } + return null; + } + + private static String stripPrefix(String name) { + int prefixSeparatorIndex = name.indexOf(':'); + return prefixSeparatorIndex == -1 ? name : name.substring(prefixSeparatorIndex + 1); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java index faedaaf273..77ca936a90 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java @@ -50,10 +50,8 @@ public final class ColorInfo implements Parcelable { @C.ColorTransfer public final int colorTransfer; - /** - * HdrStaticInfo as defined in CTA-861.3. - */ - public final byte[] hdrStaticInfo; + /** HdrStaticInfo as defined in CTA-861.3, or null if none specified. */ + public final @Nullable byte[] hdrStaticInfo; // Lazily initialized hashcode. private int hashCode; @@ -64,10 +62,13 @@ public final class ColorInfo implements Parcelable { * @param colorSpace The color space of the video. * @param colorRange The color range of the video. * @param colorTransfer The color transfer characteristics of the video. - * @param hdrStaticInfo HdrStaticInfo as defined in CTA-861.3. + * @param hdrStaticInfo HdrStaticInfo as defined in CTA-861.3, or null if none specified. */ - public ColorInfo(@C.ColorSpace int colorSpace, @C.ColorRange int colorRange, - @C.ColorTransfer int colorTransfer, byte[] hdrStaticInfo) { + public ColorInfo( + @C.ColorSpace int colorSpace, + @C.ColorRange int colorRange, + @C.ColorTransfer int colorTransfer, + @Nullable byte[] hdrStaticInfo) { this.colorSpace = colorSpace; this.colorRange = colorRange; this.colorTransfer = colorTransfer; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index 2f41831a5e..996e6f30ae 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -156,7 +156,7 @@ public final class DummySurface extends Surface { private static final int MSG_INIT = 1; private static final int MSG_RELEASE = 2; - private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexure; + private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexture; private @MonotonicNonNull Handler handler; private @Nullable Error initError; private @Nullable RuntimeException initException; @@ -169,7 +169,7 @@ public final class DummySurface extends Surface { public DummySurface init(@SecureMode int secureMode) { start(); handler = new Handler(getLooper(), /* callback= */ this); - eglSurfaceTexure = new EGLSurfaceTexture(handler); + eglSurfaceTexture = new EGLSurfaceTexture(handler); boolean wasInterrupted = false; synchronized (this) { handler.obtainMessage(MSG_INIT, secureMode, 0).sendToTarget(); @@ -232,16 +232,16 @@ public final class DummySurface extends Surface { } private void initInternal(@SecureMode int secureMode) { - Assertions.checkNotNull(eglSurfaceTexure); - eglSurfaceTexure.init(secureMode); + Assertions.checkNotNull(eglSurfaceTexture); + eglSurfaceTexture.init(secureMode); this.surface = new DummySurface( - this, eglSurfaceTexure.getSurfaceTexture(), secureMode != SECURE_MODE_NONE); + this, eglSurfaceTexture.getSurfaceTexture(), secureMode != SECURE_MODE_NONE); } private void releaseInternal() { - Assertions.checkNotNull(eglSurfaceTexure); - eglSurfaceTexure.release(); + Assertions.checkNotNull(eglSurfaceTexture); + eglSurfaceTexture.release(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java b/library/core/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java index 0982589866..089ff6343f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.video; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -26,7 +27,7 @@ import java.util.List; */ public final class HevcConfig { - public final List initializationData; + public final @Nullable List initializationData; public final int nalUnitLengthFieldLength; /** @@ -82,7 +83,7 @@ public final class HevcConfig { } } - private HevcConfig(List initializationData, int nalUnitLengthFieldLength) { + private HevcConfig(@Nullable List initializationData, int nalUnitLengthFieldLength) { this.initializationData = initializationData; this.nalUnitLengthFieldLength = nalUnitLengthFieldLength; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 579f7c45f4..181232b7b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -51,6 +51,7 @@ import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher; import java.nio.ByteBuffer; +import java.util.List; /** * Decodes and renders video using {@link MediaCodec}. @@ -83,6 +84,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // Generally there is zero or one pending output stream offset. We track more offsets to allow for // pending output streams that have fewer frames than the codec latency. private static final int MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT = 10; + /** + * Scale factor for the initial maximum input size used to configure the codec in non-adaptive + * playbacks. See {@link #getCodecMaxValues(MediaCodecInfo, Format, Format[])}. + */ + private static final float INITIAL_FORMAT_MAX_INPUT_SIZE_SCALE_FACTOR = 1.5f; + + private static boolean evaluatedDeviceNeedsSetOutputSurfaceWorkaround; + private static boolean deviceNeedsSetOutputSurfaceWorkaround; private final Context context; private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper; @@ -201,11 +210,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) { - super(C.TRACK_TYPE_VIDEO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); + super( + C.TRACK_TYPE_VIDEO, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + /* assumedMinimumCodecOperatingRate= */ 30); this.allowedJoiningTimeMs = allowedJoiningTimeMs; this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; this.context = context.getApplicationContext(); - frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context); + frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(this.context); eventDispatcher = new EventDispatcher(eventHandler, eventListener); deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround(); pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; @@ -236,32 +250,28 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption; } } - MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, - requiresSecureDecryption); - if (decoderInfo == null) { - return requiresSecureDecryption && mediaCodecSelector.getDecoderInfo(mimeType, false) != null - ? FORMAT_UNSUPPORTED_DRM : FORMAT_UNSUPPORTED_SUBTYPE; + List decoderInfos = + mediaCodecSelector.getDecoderInfos(format.sampleMimeType, requiresSecureDecryption); + if (decoderInfos.isEmpty()) { + return requiresSecureDecryption + && !mediaCodecSelector + .getDecoderInfos(format.sampleMimeType, /* requiresSecureDecoder= */ false) + .isEmpty() + ? FORMAT_UNSUPPORTED_DRM + : FORMAT_UNSUPPORTED_SUBTYPE; } if (!supportsFormatDrm(drmSessionManager, drmInitData)) { return FORMAT_UNSUPPORTED_DRM; } - boolean decoderCapable = decoderInfo.isCodecSupported(format.codecs); - if (decoderCapable && format.width > 0 && format.height > 0) { - if (Util.SDK_INT >= 21) { - decoderCapable = decoderInfo.isVideoSizeAndRateSupportedV21(format.width, format.height, - format.frameRate); - } else { - decoderCapable = format.width * format.height <= MediaCodecUtil.maxH264DecodableFrameSize(); - if (!decoderCapable) { - Log.d(TAG, "FalseCheck [legacyFrameSize, " + format.width + "x" + format.height + "] [" - + Util.DEVICE_DEBUG_INFO + "]"); - } - } - } - - int adaptiveSupport = decoderInfo.adaptive ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS; + // Check capabilities for the first decoder in the list, which takes priority. + MediaCodecInfo decoderInfo = decoderInfos.get(0); + boolean isFormatSupported = decoderInfo.isFormatSupported(format); + int adaptiveSupport = + decoderInfo.isSeamlessAdaptationSupported(format) + ? ADAPTIVE_SEAMLESS + : ADAPTIVE_NOT_SEAMLESS; int tunnelingSupport = decoderInfo.tunneling ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; - int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; + int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; return adaptiveSupport | tunnelingSupport | formatSupport; } @@ -435,11 +445,21 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, - MediaCrypto crypto) throws DecoderQueryException { + protected void configureCodec( + MediaCodecInfo codecInfo, + MediaCodec codec, + Format format, + MediaCrypto crypto, + float codecOperatingRate) + throws DecoderQueryException { codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); - MediaFormat mediaFormat = getMediaFormat(format, codecMaxValues, deviceNeedsAutoFrcWorkaround, - tunnelingAudioSessionId); + MediaFormat mediaFormat = + getMediaFormat( + format, + codecMaxValues, + codecOperatingRate, + deviceNeedsAutoFrcWorkaround, + tunnelingAudioSessionId); if (surface == null) { Assertions.checkState(shouldUseDummySurface(codecInfo)); if (dummySurface == null) { @@ -456,10 +476,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override protected @KeepCodecResult int canKeepCodec( MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { - if (areAdaptationCompatible(codecInfo.adaptive, oldFormat, newFormat) + if (codecInfo.isSeamlessAdaptationSupported(oldFormat, newFormat) && newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height - && getMaxInputSize(newFormat) <= codecMaxValues.inputSize) { + && getMaxInputSize(codecInfo, newFormat) <= codecMaxValues.inputSize) { return oldFormat.initializationDataEquals(newFormat) ? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION : KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION; @@ -491,6 +511,21 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { buffersInCodecCount = 0; } + @Override + protected float getCodecOperatingRate( + float operatingRate, Format format, Format[] streamFormats) { + // Use the highest known stream frame-rate up front, to avoid having to reconfigure the codec + // should an adaptive switch to that stream occur. + float maxFrameRate = -1; + for (Format streamFormat : streamFormats) { + float streamFrameRate = streamFormat.frameRate; + if (streamFrameRate != Format.NO_VALUE) { + maxFrameRate = Math.max(maxFrameRate, streamFrameRate); + } + } + return maxFrameRate == -1 ? CODEC_OPERATING_RATE_UNSET : (maxFrameRate * operatingRate); + } + @Override protected void onCodecInitialized(String name, long initializedTimestampMs, long initializationDurationMs) { @@ -644,6 +679,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return false; } + /** + * Returns the offset that should be subtracted from {@code bufferPresentationTimeUs} in {@link + * #processOutputBuffer(long, long, MediaCodec, ByteBuffer, int, int, long, boolean)} to get the + * playback position with respect to the media. + */ + protected long getOutputStreamOffsetUs() { + return outputStreamOffsetUs; + } + /** * Called when an output buffer is successfully processed. * @@ -926,6 +970,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * * @param format The format of media. * @param codecMaxValues Codec max values that should be used when configuring the decoder. + * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if + * no codec operating rate should be set. * @param deviceNeedsAutoFrcWorkaround Whether the device is known to enable frame-rate conversion * logic that negatively impacts ExoPlayer. * @param tunnelingAudioSessionId The audio session id to use for tunneling, or {@link @@ -936,6 +982,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { protected MediaFormat getMediaFormat( Format format, CodecMaxValues codecMaxValues, + float codecOperatingRate, boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) { MediaFormat mediaFormat = new MediaFormat(); @@ -956,6 +1003,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // Set codec configuration values. if (Util.SDK_INT >= 23) { mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */); + if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET) { + mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate); + } } if (deviceNeedsAutoFrcWorkaround) { mediaFormat.setInteger("auto-frc", 0); @@ -981,20 +1031,33 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { throws DecoderQueryException { int maxWidth = format.width; int maxHeight = format.height; - int maxInputSize = getMaxInputSize(format); + int maxInputSize = getMaxInputSize(codecInfo, format); if (streamFormats.length == 1) { // The single entry in streamFormats must correspond to the format for which the codec is // being configured. + if (maxInputSize != Format.NO_VALUE) { + int codecMaxInputSize = + getCodecMaxInputSize(codecInfo, format.sampleMimeType, format.width, format.height); + if (codecMaxInputSize != Format.NO_VALUE) { + // Scale up the initial video decoder maximum input size so playlist item transitions with + // small increases in maximum sample size don't require reinitialization. This only makes + // a difference if the exact maximum sample sizes are known from the container. + int scaledMaxInputSize = + (int) (maxInputSize * INITIAL_FORMAT_MAX_INPUT_SIZE_SCALE_FACTOR); + // Avoid exceeding the maximum expected for the codec. + maxInputSize = Math.min(scaledMaxInputSize, codecMaxInputSize); + } + } return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); } boolean haveUnknownDimensions = false; for (Format streamFormat : streamFormats) { - if (areAdaptationCompatible(codecInfo.adaptive, format, streamFormat)) { + if (codecInfo.isSeamlessAdaptationSupported(format, streamFormat)) { haveUnknownDimensions |= (streamFormat.width == Format.NO_VALUE || streamFormat.height == Format.NO_VALUE); maxWidth = Math.max(maxWidth, streamFormat.width); maxHeight = Math.max(maxHeight, streamFormat.height); - maxInputSize = Math.max(maxInputSize, getMaxInputSize(streamFormat)); + maxInputSize = Math.max(maxInputSize, getMaxInputSize(codecInfo, streamFormat)); } } if (haveUnknownDimensions) { @@ -1004,7 +1067,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { maxWidth = Math.max(maxWidth, codecMaxSize.x); maxHeight = Math.max(maxHeight, codecMaxSize.y); maxInputSize = - Math.max(maxInputSize, getMaxInputSize(format.sampleMimeType, maxWidth, maxHeight)); + Math.max( + maxInputSize, + getCodecMaxInputSize(codecInfo, format.sampleMimeType, maxWidth, maxHeight)); Log.w(TAG, "Codec max resolution adjusted to: " + maxWidth + "x" + maxHeight); } } @@ -1053,13 +1118,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } /** - * Returns a maximum input buffer size for a given format. + * Returns a maximum input buffer size for a given codec and format. * + * @param codecInfo Information about the {@link MediaCodec} being configured. * @param format The format. * @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not * be determined. */ - private static int getMaxInputSize(Format format) { + private static int getMaxInputSize(MediaCodecInfo codecInfo, Format format) { if (format.maxInputSize != Format.NO_VALUE) { // The format defines an explicit maximum input size. Add the total size of initialization // data buffers, as they may need to be queued in the same input buffer as the largest sample. @@ -1072,20 +1138,22 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } else { // Calculated maximum input sizes are overestimates, so it's not necessary to add the size of // initialization data. - return getMaxInputSize(format.sampleMimeType, format.width, format.height); + return getCodecMaxInputSize(codecInfo, format.sampleMimeType, format.width, format.height); } } /** - * Returns a maximum input size for a given mime type, width and height. + * Returns a maximum input size for a given codec, MIME type, width and height. * + * @param codecInfo Information about the {@link MediaCodec} being configured. * @param sampleMimeType The format mime type. * @param width The width in pixels. * @param height The height in pixels. * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be * determined. */ - private static int getMaxInputSize(String sampleMimeType, int width, int height) { + private static int getCodecMaxInputSize( + MediaCodecInfo codecInfo, String sampleMimeType, int width, int height) { if (width == Format.NO_VALUE || height == Format.NO_VALUE) { // We can't infer a maximum input size without video dimensions. return Format.NO_VALUE; @@ -1101,9 +1169,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { minCompressionRatio = 2; break; case MimeTypes.VIDEO_H264: - if ("BRAVIA 4K 2015".equals(Util.MODEL)) { - // The Sony BRAVIA 4k TV has input buffers that are too small for the calculated 4k video - // maximum input size, so use the default value. + if ("BRAVIA 4K 2015".equals(Util.MODEL) // Sony Bravia 4K + || ("Amazon".equals(Util.MANUFACTURER) + && ("KFSOWI".equals(Util.MODEL) // Kindle Soho + || ("AFTS".equals(Util.MODEL) && codecInfo.secure)))) { // Fire TV Gen 2 + // Use the default value for cases where platform limitations may prevent buffers of the + // calculated maximum input size from being allocated. return Format.NO_VALUE; } // Round up width/height to an integer number of macroblocks. @@ -1128,23 +1199,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return (maxPixels * 3) / (2 * minCompressionRatio); } - /** - * Returns whether a codec with suitable {@link CodecMaxValues} will support adaptation between - * two {@link Format}s. - * - * @param codecIsAdaptive Whether the codec supports seamless resolution switches. - * @param first The first format. - * @param second The second format. - * @return Whether the codec will support adaptation between the two {@link Format}s. - */ - private static boolean areAdaptationCompatible( - boolean codecIsAdaptive, Format first, Format second) { - return first.sampleMimeType.equals(second.sampleMimeType) - && first.rotationDegrees == second.rotationDegrees - && (codecIsAdaptive || (first.width == second.width && first.height == second.height)) - && Util.areEqual(first.colorInfo, second.colorInfo); - } - /** * Returns whether the device is known to enable frame-rate conversion logic that negatively * impacts ExoPlayer. @@ -1163,42 +1217,188 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER); } - /** - * Returns whether the device is known to implement {@link MediaCodec#setOutputSurface(Surface)} - * incorrectly. - *

    - * If true is returned then we fall back to releasing and re-instantiating the codec instead. + /* + * TODO: + * + * 1. Validate that Android device certification now ensures correct behavior, and add a + * corresponding SDK_INT upper bound for applying the workaround (probably SDK_INT < 26). + * 2. Determine a complete list of affected devices. + * 3. Some of the devices in this list only fail to support setOutputSurface when switching from + * a SurfaceView provided Surface to a Surface of another type (e.g. TextureView/DummySurface), + * and vice versa. One hypothesis is that setOutputSurface fails when the surfaces have + * different pixel formats. If we can find a way to query the Surface instances to determine + * whether this case applies, then we'll be able to provide a more targeted workaround. */ - private static boolean codecNeedsSetOutputSurfaceWorkaround(String name) { - // Work around https://github.com/google/ExoPlayer/issues/3236, + /** + * Returns whether the codec is known to implement {@link MediaCodec#setOutputSurface(Surface)} + * incorrectly. + * + *

    If true is returned then we fall back to releasing and re-instantiating the codec instead. + * + * @param name The name of the codec. + * @return True if the device is known to implement {@link MediaCodec#setOutputSurface(Surface)} + * incorrectly. + */ + protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) { + if (Util.SDK_INT >= 27 || name.startsWith("OMX.google")) { + // Devices running API level 27 or later should also be unaffected. Google OMX decoders are + // not known to have this issue on any API level. + return false; + } + // Work around: + // https://github.com/google/ExoPlayer/issues/3236, // https://github.com/google/ExoPlayer/issues/3355, // https://github.com/google/ExoPlayer/issues/3439, // https://github.com/google/ExoPlayer/issues/3724, // https://github.com/google/ExoPlayer/issues/3835, // https://github.com/google/ExoPlayer/issues/4006, // https://github.com/google/ExoPlayer/issues/4084, - // https://github.com/google/ExoPlayer/issues/4104. - // https://github.com/google/ExoPlayer/issues/4134. - return (("deb".equals(Util.DEVICE) // Nexus 7 (2013) - || "flo".equals(Util.DEVICE) // Nexus 7 (2013) - || "mido".equals(Util.DEVICE) // Redmi Note 4 - || "santoni".equals(Util.DEVICE)) // Redmi 4X - && "OMX.qcom.video.decoder.avc".equals(name)) - || (("tcl_eu".equals(Util.DEVICE) // TCL Percee TV - || "SVP-DTV15".equals(Util.DEVICE) // Sony Bravia 4K 2015 - || "BRAVIA_ATV2".equals(Util.DEVICE) // Sony Bravia 4K GB - || Util.DEVICE.startsWith("panell_") // Motorola Moto C Plus - || "F3311".equals(Util.DEVICE) // Sony Xperia E5 - || "M5c".equals(Util.DEVICE) // Meizu M5C - || "QM16XE_U".equals(Util.DEVICE) // Philips QM163E - || "A7010a48".equals(Util.DEVICE) // Lenovo K4 Note - || "woods_f".equals(Util.MODEL)) // Moto E (4) - && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)) - || (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite - || "CAM-L21".equals(Util.MODEL)) // Huawei Y6II - && "OMX.k3.video.decoder.avc".equals(name)) - || (("HUAWEI VNS-L21".equals(Util.MODEL)) // Huawei P9 Lite - && "OMX.IMG.MSVDX.Decoder.AVC".equals(name)); + // https://github.com/google/ExoPlayer/issues/4104, + // https://github.com/google/ExoPlayer/issues/4134, + // https://github.com/google/ExoPlayer/issues/4315, + // https://github.com/google/ExoPlayer/issues/4419, + // https://github.com/google/ExoPlayer/issues/4460, + // https://github.com/google/ExoPlayer/issues/4468. + synchronized (MediaCodecVideoRenderer.class) { + if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) { + switch (Util.DEVICE) { + case "1601": + case "1713": + case "1714": + case "A10-70F": + case "A1601": + case "A2016a40": + case "A7000-a": + case "A7000plus": + case "A7010a48": + case "A7020a48": + case "AquaPowerM": + case "Aura_Note_2": + case "BLACK-1X": + case "BRAVIA_ATV2": + case "C1": + case "ComioS1": + case "CP8676_I02": + case "CPH1609": + case "CPY83_I00": + case "cv1": + case "cv3": + case "deb": + case "E5643": + case "ELUGA_A3_Pro": + case "ELUGA_Note": + case "ELUGA_Prim": + case "ELUGA_Ray_X": + case "EverStar_S": + case "F3111": + case "F3113": + case "F3116": + case "F3211": + case "F3213": + case "F3215": + case "F3311": + case "flo": + case "GiONEE_CBL7513": + case "GiONEE_GBL7319": + case "GIONEE_GBL7360": + case "GIONEE_SWW1609": + case "GIONEE_SWW1627": + case "GIONEE_SWW1631": + case "GIONEE_WBL5708": + case "GIONEE_WBL7365": + case "GIONEE_WBL7519": + case "griffin": + case "htc_e56ml_dtul": + case "hwALE-H": + case "HWBLN-H": + case "HWCAM-H": + case "HWVNS-H": + case "iball8735_9806": + case "Infinix-X572": + case "iris60": + case "itel_S41": + case "j2xlteins": + case "JGZ": + case "K50a40": + case "le_x6": + case "LS-5017": + case "M5c": + case "manning": + case "marino_f": + case "MEIZU_M5": + case "mh": + case "mido": + case "MX6": + case "namath": + case "nicklaus_f": + case "NX541J": + case "NX573J": + case "OnePlus5T": + case "p212": + case "P681": + case "P85": + case "panell_d": + case "panell_dl": + case "panell_ds": + case "panell_dt": + case "PB2-670M": + case "PGN528": + case "PGN610": + case "PGN611": + case "Phantom6": + case "Pixi4-7_3G": + case "Pixi5-10_4G": + case "PLE": + case "PRO7S": + case "Q350": + case "Q4260": + case "Q427": + case "Q4310": + case "Q5": + case "QM16XE_U": + case "QX1": + case "santoni": + case "Slate_Pro": + case "SVP-DTV15": + case "s905x018": + case "taido_row": + case "TB3-730F": + case "TB3-730X": + case "TB3-850F": + case "TB3-850M": + case "tcl_eu": + case "V1": + case "V23GB": + case "V5": + case "vernee_M5": + case "watson": + case "whyred": + case "woods_f": + case "woods_fn": + case "X3_HK": + case "XE2X": + case "XT1663": + case "Z12_PRO": + case "Z80": + deviceNeedsSetOutputSurfaceWorkaround = true; + break; + default: + // Do nothing. + break; + } + switch (Util.MODEL) { + case "AFTA": + case "AFTN": + deviceNeedsSetOutputSurfaceWorkaround = true; + break; + default: + // Do nothing. + break; + } + evaluatedDeviceNeedsSetOutputSurfaceWorkaround = true; + } + } + return deviceNeedsSetOutputSurfaceWorkaround; } protected static final class CodecMaxValues { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java index 9036b19a75..3c0fb92191 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java @@ -72,8 +72,12 @@ public final class VideoFrameReleaseTimeHelper { * @param context A context from which information about the default display can be retrieved. */ public VideoFrameReleaseTimeHelper(@Nullable Context context) { - windowManager = context == null ? null - : (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + if (context != null) { + context = context.getApplicationContext(); + windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + } else { + windowManager = null; + } if (windowManager != null) { displayListener = Util.SDK_INT >= 17 ? maybeBuildDefaultDisplayListenerV17(context) : null; vsyncSampler = VSyncSampler.getInstance(); @@ -287,7 +291,7 @@ public final class VideoFrameReleaseTimeHelper { sampledVsyncTimeNs = C.TIME_UNSET; choreographerOwnerThread = new HandlerThread("ChoreographerOwner:Handler"); choreographerOwnerThread.start(); - handler = new Handler(choreographerOwnerThread.getLooper(), this); + handler = Util.createHandler(choreographerOwnerThread.getLooper(), /* callback= */ this); handler.sendEmptyMessage(CREATE_CHOREOGRAPHER); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoListener.java index ab09e0bbc2..6f492c3975 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoListener.java @@ -34,12 +34,25 @@ public interface VideoListener { * square pixels this will be equal to 1.0. Different values are indicative of anamorphic * content. */ - void onVideoSizeChanged( - int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio); + default void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {} + + /** + * Called each time there's a change in the size of the surface onto which the video is being + * rendered. + * + * @param width The surface width in pixels. May be {@link + * com.google.android.exoplayer2.C#LENGTH_UNSET} if unknown, or 0 if the video is not rendered + * onto a surface. + * @param height The surface height in pixels. May be {@link + * com.google.android.exoplayer2.C#LENGTH_UNSET} if unknown, or 0 if the video is not rendered + * onto a surface. + */ + default void onSurfaceSizeChanged(int width, int height) {} /** * Called when a frame is rendered for the first time since setting the surface, and when a frame * is rendered for the first time since a video track was selected. */ - void onRenderedFirstFrame(); + default void onRenderedFirstFrame() {} } diff --git a/library/core/src/test/assets/amr/sample_nb_cbr.amr b/library/core/src/test/assets/amr/sample_nb_cbr.amr new file mode 100644 index 0000000000..2e21cc843c Binary files /dev/null and b/library/core/src/test/assets/amr/sample_nb_cbr.amr differ diff --git a/library/core/src/test/assets/amr/sample_nb_cbr.amr.0.dump b/library/core/src/test/assets/amr/sample_nb_cbr.amr.0.dump new file mode 100644 index 0000000000..e8ba3c3588 --- /dev/null +++ b/library/core/src/test/assets/amr/sample_nb_cbr.amr.0.dump @@ -0,0 +1,902 @@ +seekMap: + isSeekable = true + duration = 4360000 + getPosition(0) = [[timeUs=0, position=6]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = null + containerMimeType = null + sampleMimeType = audio/3gpp + maxInputSize = 61 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 8000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 2834 + sample count = 218 + sample 0: + time = 0 + flags = 1 + data = length 13, hash 371B046C + sample 1: + time = 20000 + flags = 1 + data = length 13, hash CE30BF5B + sample 2: + time = 40000 + flags = 1 + data = length 13, hash 19A59975 + sample 3: + time = 60000 + flags = 1 + data = length 13, hash 4879773C + sample 4: + time = 80000 + flags = 1 + data = length 13, hash E8F83019 + sample 5: + time = 100000 + flags = 1 + data = length 13, hash D265CDC9 + sample 6: + time = 120000 + flags = 1 + data = length 13, hash 91653DAA + sample 7: + time = 140000 + flags = 1 + data = length 13, hash C79456F6 + sample 8: + time = 160000 + flags = 1 + data = length 13, hash CDDC4422 + sample 9: + time = 180000 + flags = 1 + data = length 13, hash D9ED3AF1 + sample 10: + time = 200000 + flags = 1 + data = length 13, hash BAB75A33 + sample 11: + time = 220000 + flags = 1 + data = length 13, hash 2221B4FF + sample 12: + time = 240000 + flags = 1 + data = length 13, hash 96400A0B + sample 13: + time = 260000 + flags = 1 + data = length 13, hash 582E6FB + sample 14: + time = 280000 + flags = 1 + data = length 13, hash C4E878E5 + sample 15: + time = 300000 + flags = 1 + data = length 13, hash C849A1BD + sample 16: + time = 320000 + flags = 1 + data = length 13, hash CFA8A9ED + sample 17: + time = 340000 + flags = 1 + data = length 13, hash 70CA4907 + sample 18: + time = 360000 + flags = 1 + data = length 13, hash B47D4454 + sample 19: + time = 380000 + flags = 1 + data = length 13, hash 282998C1 + sample 20: + time = 400000 + flags = 1 + data = length 13, hash 3F3F7A65 + sample 21: + time = 420000 + flags = 1 + data = length 13, hash CC2EAB58 + sample 22: + time = 440000 + flags = 1 + data = length 13, hash 279EF712 + sample 23: + time = 460000 + flags = 1 + data = length 13, hash AA2F4B29 + sample 24: + time = 480000 + flags = 1 + data = length 13, hash F6F658C4 + sample 25: + time = 500000 + flags = 1 + data = length 13, hash D7DEBD17 + sample 26: + time = 520000 + flags = 1 + data = length 13, hash 6DAB9A17 + sample 27: + time = 540000 + flags = 1 + data = length 13, hash 6ECE1571 + sample 28: + time = 560000 + flags = 1 + data = length 13, hash B3D0507F + sample 29: + time = 580000 + flags = 1 + data = length 13, hash 21E356B9 + sample 30: + time = 600000 + flags = 1 + data = length 13, hash 410EA12 + sample 31: + time = 620000 + flags = 1 + data = length 13, hash 533895A8 + sample 32: + time = 640000 + flags = 1 + data = length 13, hash C61B3E5A + sample 33: + time = 660000 + flags = 1 + data = length 13, hash 982170E6 + sample 34: + time = 680000 + flags = 1 + data = length 13, hash 7A0468C5 + sample 35: + time = 700000 + flags = 1 + data = length 13, hash 9C85EAA7 + sample 36: + time = 720000 + flags = 1 + data = length 13, hash B6B341B6 + sample 37: + time = 740000 + flags = 1 + data = length 13, hash 6937532E + sample 38: + time = 760000 + flags = 1 + data = length 13, hash 8CF2A3A0 + sample 39: + time = 780000 + flags = 1 + data = length 13, hash D2682AC6 + sample 40: + time = 800000 + flags = 1 + data = length 13, hash BBC5710F + sample 41: + time = 820000 + flags = 1 + data = length 13, hash 59080B6C + sample 42: + time = 840000 + flags = 1 + data = length 13, hash E4118291 + sample 43: + time = 860000 + flags = 1 + data = length 13, hash A1E5B296 + sample 44: + time = 880000 + flags = 1 + data = length 13, hash D7B8F95B + sample 45: + time = 900000 + flags = 1 + data = length 13, hash CC839BE1 + sample 46: + time = 920000 + flags = 1 + data = length 13, hash D459DFCE + sample 47: + time = 940000 + flags = 1 + data = length 13, hash D6AD19EC + sample 48: + time = 960000 + flags = 1 + data = length 13, hash D05E373D + sample 49: + time = 980000 + flags = 1 + data = length 13, hash 6A4460C7 + sample 50: + time = 1000000 + flags = 1 + data = length 13, hash C9A0D93F + sample 51: + time = 1020000 + flags = 1 + data = length 13, hash 3FA819E7 + sample 52: + time = 1040000 + flags = 1 + data = length 13, hash 1D3CBDFC + sample 53: + time = 1060000 + flags = 1 + data = length 13, hash 8BBBB403 + sample 54: + time = 1080000 + flags = 1 + data = length 13, hash 21B4A0F9 + sample 55: + time = 1100000 + flags = 1 + data = length 13, hash C0F921D1 + sample 56: + time = 1120000 + flags = 1 + data = length 13, hash 5D812AAB + sample 57: + time = 1140000 + flags = 1 + data = length 13, hash 50C9F3F8 + sample 58: + time = 1160000 + flags = 1 + data = length 13, hash 5C2BB5D1 + sample 59: + time = 1180000 + flags = 1 + data = length 13, hash 6BF9BEA5 + sample 60: + time = 1200000 + flags = 1 + data = length 13, hash 2738C1E6 + sample 61: + time = 1220000 + flags = 1 + data = length 13, hash 5FC288A6 + sample 62: + time = 1240000 + flags = 1 + data = length 13, hash 7E8E442A + sample 63: + time = 1260000 + flags = 1 + data = length 13, hash AEAA2BBA + sample 64: + time = 1280000 + flags = 1 + data = length 13, hash 4E2ACD2F + sample 65: + time = 1300000 + flags = 1 + data = length 13, hash D6C90ACF + sample 66: + time = 1320000 + flags = 1 + data = length 13, hash 6FD8A944 + sample 67: + time = 1340000 + flags = 1 + data = length 13, hash A835BBF9 + sample 68: + time = 1360000 + flags = 1 + data = length 13, hash F7713830 + sample 69: + time = 1380000 + flags = 1 + data = length 13, hash 3AA966E5 + sample 70: + time = 1400000 + flags = 1 + data = length 13, hash F939E829 + sample 71: + time = 1420000 + flags = 1 + data = length 13, hash 7676DE49 + sample 72: + time = 1440000 + flags = 1 + data = length 13, hash 93BB890A + sample 73: + time = 1460000 + flags = 1 + data = length 13, hash B57DBEC8 + sample 74: + time = 1480000 + flags = 1 + data = length 13, hash 66B0A5B6 + sample 75: + time = 1500000 + flags = 1 + data = length 13, hash D733E0D + sample 76: + time = 1520000 + flags = 1 + data = length 13, hash 80941726 + sample 77: + time = 1540000 + flags = 1 + data = length 13, hash 556ED633 + sample 78: + time = 1560000 + flags = 1 + data = length 13, hash C5EDF4E1 + sample 79: + time = 1580000 + flags = 1 + data = length 13, hash 6B287445 + sample 80: + time = 1600000 + flags = 1 + data = length 13, hash DC97C4A7 + sample 81: + time = 1620000 + flags = 1 + data = length 13, hash DA8CBDF4 + sample 82: + time = 1640000 + flags = 1 + data = length 13, hash 6F60FF77 + sample 83: + time = 1660000 + flags = 1 + data = length 13, hash 3EB22B96 + sample 84: + time = 1680000 + flags = 1 + data = length 13, hash B3C31AF5 + sample 85: + time = 1700000 + flags = 1 + data = length 13, hash 1854AA92 + sample 86: + time = 1720000 + flags = 1 + data = length 13, hash 6488264B + sample 87: + time = 1740000 + flags = 1 + data = length 13, hash 4CC8C5C1 + sample 88: + time = 1760000 + flags = 1 + data = length 13, hash 19CC7523 + sample 89: + time = 1780000 + flags = 1 + data = length 13, hash 9BE7B928 + sample 90: + time = 1800000 + flags = 1 + data = length 13, hash 47EC7CFD + sample 91: + time = 1820000 + flags = 1 + data = length 13, hash EC940120 + sample 92: + time = 1840000 + flags = 1 + data = length 13, hash 73BDA6D0 + sample 93: + time = 1860000 + flags = 1 + data = length 13, hash FACB3314 + sample 94: + time = 1880000 + flags = 1 + data = length 13, hash EC61D13B + sample 95: + time = 1900000 + flags = 1 + data = length 13, hash B28C7B6C + sample 96: + time = 1920000 + flags = 1 + data = length 13, hash B1A4CECD + sample 97: + time = 1940000 + flags = 1 + data = length 13, hash 56D41BA6 + sample 98: + time = 1960000 + flags = 1 + data = length 13, hash 90499F4 + sample 99: + time = 1980000 + flags = 1 + data = length 13, hash 65D9A9D3 + sample 100: + time = 2000000 + flags = 1 + data = length 13, hash D9004CC + sample 101: + time = 2020000 + flags = 1 + data = length 13, hash 4139C6ED + sample 102: + time = 2040000 + flags = 1 + data = length 13, hash C4F8097C + sample 103: + time = 2060000 + flags = 1 + data = length 13, hash 94D424FA + sample 104: + time = 2080000 + flags = 1 + data = length 13, hash C2C6F5FD + sample 105: + time = 2100000 + flags = 1 + data = length 13, hash 15719008 + sample 106: + time = 2120000 + flags = 1 + data = length 13, hash 4F64F524 + sample 107: + time = 2140000 + flags = 1 + data = length 13, hash F9E01C1E + sample 108: + time = 2160000 + flags = 1 + data = length 13, hash 74C4EE74 + sample 109: + time = 2180000 + flags = 1 + data = length 13, hash 7EE7553D + sample 110: + time = 2200000 + flags = 1 + data = length 13, hash 62DE6539 + sample 111: + time = 2220000 + flags = 1 + data = length 13, hash 7F5EC222 + sample 112: + time = 2240000 + flags = 1 + data = length 13, hash 644067F + sample 113: + time = 2260000 + flags = 1 + data = length 13, hash CDF6C9DC + sample 114: + time = 2280000 + flags = 1 + data = length 13, hash 8B5DBC80 + sample 115: + time = 2300000 + flags = 1 + data = length 13, hash AD4BBA03 + sample 116: + time = 2320000 + flags = 1 + data = length 13, hash 7A76340 + sample 117: + time = 2340000 + flags = 1 + data = length 13, hash 3610F5B0 + sample 118: + time = 2360000 + flags = 1 + data = length 13, hash 430BC60B + sample 119: + time = 2380000 + flags = 1 + data = length 13, hash 99CF1CA6 + sample 120: + time = 2400000 + flags = 1 + data = length 13, hash 1331C70B + sample 121: + time = 2420000 + flags = 1 + data = length 13, hash BD76E69D + sample 122: + time = 2440000 + flags = 1 + data = length 13, hash 5DA652AC + sample 123: + time = 2460000 + flags = 1 + data = length 13, hash 3B7BF6CE + sample 124: + time = 2480000 + flags = 1 + data = length 13, hash ABBFD143 + sample 125: + time = 2500000 + flags = 1 + data = length 13, hash E9447166 + sample 126: + time = 2520000 + flags = 1 + data = length 13, hash EC40068C + sample 127: + time = 2540000 + flags = 1 + data = length 13, hash A2869400 + sample 128: + time = 2560000 + flags = 1 + data = length 13, hash C7E0746B + sample 129: + time = 2580000 + flags = 1 + data = length 13, hash 60601BB1 + sample 130: + time = 2600000 + flags = 1 + data = length 13, hash 975AAE9B + sample 131: + time = 2620000 + flags = 1 + data = length 13, hash 8BBC0EB2 + sample 132: + time = 2640000 + flags = 1 + data = length 13, hash 57FB39E5 + sample 133: + time = 2660000 + flags = 1 + data = length 13, hash 4CDCEEDB + sample 134: + time = 2680000 + flags = 1 + data = length 13, hash EA16E256 + sample 135: + time = 2700000 + flags = 1 + data = length 13, hash 287E7D9E + sample 136: + time = 2720000 + flags = 1 + data = length 13, hash 55AB8FB9 + sample 137: + time = 2740000 + flags = 1 + data = length 13, hash 129890EF + sample 138: + time = 2760000 + flags = 1 + data = length 13, hash 90834F57 + sample 139: + time = 2780000 + flags = 1 + data = length 13, hash 5B3228E0 + sample 140: + time = 2800000 + flags = 1 + data = length 13, hash DD19E175 + sample 141: + time = 2820000 + flags = 1 + data = length 13, hash EE7EA342 + sample 142: + time = 2840000 + flags = 1 + data = length 13, hash DB3AF473 + sample 143: + time = 2860000 + flags = 1 + data = length 13, hash 25AEC43F + sample 144: + time = 2880000 + flags = 1 + data = length 13, hash EE9BF97F + sample 145: + time = 2900000 + flags = 1 + data = length 13, hash FFFBE047 + sample 146: + time = 2920000 + flags = 1 + data = length 13, hash BEACFCB0 + sample 147: + time = 2940000 + flags = 1 + data = length 13, hash AEB5096C + sample 148: + time = 2960000 + flags = 1 + data = length 13, hash B0D381B + sample 149: + time = 2980000 + flags = 1 + data = length 13, hash 3D9D5122 + sample 150: + time = 3000000 + flags = 1 + data = length 13, hash 6C1DDB95 + sample 151: + time = 3020000 + flags = 1 + data = length 13, hash ADACADCF + sample 152: + time = 3040000 + flags = 1 + data = length 13, hash 159E321E + sample 153: + time = 3060000 + flags = 1 + data = length 13, hash B1466264 + sample 154: + time = 3080000 + flags = 1 + data = length 13, hash 4DDF7223 + sample 155: + time = 3100000 + flags = 1 + data = length 13, hash C9BDB82A + sample 156: + time = 3120000 + flags = 1 + data = length 13, hash A49B2D9D + sample 157: + time = 3140000 + flags = 1 + data = length 13, hash D645E7E5 + sample 158: + time = 3160000 + flags = 1 + data = length 13, hash 1C4232DC + sample 159: + time = 3180000 + flags = 1 + data = length 13, hash 83078219 + sample 160: + time = 3200000 + flags = 1 + data = length 13, hash D6D8B072 + sample 161: + time = 3220000 + flags = 1 + data = length 13, hash 975DB40 + sample 162: + time = 3240000 + flags = 1 + data = length 13, hash A15FDD05 + sample 163: + time = 3260000 + flags = 1 + data = length 13, hash 4B839E41 + sample 164: + time = 3280000 + flags = 1 + data = length 13, hash 7418F499 + sample 165: + time = 3300000 + flags = 1 + data = length 13, hash 7A4945E4 + sample 166: + time = 3320000 + flags = 1 + data = length 13, hash 6249558C + sample 167: + time = 3340000 + flags = 1 + data = length 13, hash BD4C5BE3 + sample 168: + time = 3360000 + flags = 1 + data = length 13, hash BAB30F1D + sample 169: + time = 3380000 + flags = 1 + data = length 13, hash 1E1C7012 + sample 170: + time = 3400000 + flags = 1 + data = length 13, hash 9A3F8A89 + sample 171: + time = 3420000 + flags = 1 + data = length 13, hash 20BE6D7B + sample 172: + time = 3440000 + flags = 1 + data = length 13, hash CAA0591D + sample 173: + time = 3460000 + flags = 1 + data = length 13, hash 6D554D17 + sample 174: + time = 3480000 + flags = 1 + data = length 13, hash D97C3B31 + sample 175: + time = 3500000 + flags = 1 + data = length 13, hash 75BC5C3 + sample 176: + time = 3520000 + flags = 1 + data = length 13, hash 7BA1784B + sample 177: + time = 3540000 + flags = 1 + data = length 13, hash 1D175D92 + sample 178: + time = 3560000 + flags = 1 + data = length 13, hash ADCA60FD + sample 179: + time = 3580000 + flags = 1 + data = length 13, hash 37018693 + sample 180: + time = 3600000 + flags = 1 + data = length 13, hash 4553606F + sample 181: + time = 3620000 + flags = 1 + data = length 13, hash CF434565 + sample 182: + time = 3640000 + flags = 1 + data = length 13, hash D264D757 + sample 183: + time = 3660000 + flags = 1 + data = length 13, hash 4FB493EF + sample 184: + time = 3680000 + flags = 1 + data = length 13, hash 919F53A + sample 185: + time = 3700000 + flags = 1 + data = length 13, hash C22B009B + sample 186: + time = 3720000 + flags = 1 + data = length 13, hash 5981470 + sample 187: + time = 3740000 + flags = 1 + data = length 13, hash A5D3937C + sample 188: + time = 3760000 + flags = 1 + data = length 13, hash A2504429 + sample 189: + time = 3780000 + flags = 1 + data = length 13, hash AD1B70BE + sample 190: + time = 3800000 + flags = 1 + data = length 13, hash 2E39ED5E + sample 191: + time = 3820000 + flags = 1 + data = length 13, hash 13A8BE8E + sample 192: + time = 3840000 + flags = 1 + data = length 13, hash 1ACD740B + sample 193: + time = 3860000 + flags = 1 + data = length 13, hash 80F38B3 + sample 194: + time = 3880000 + flags = 1 + data = length 13, hash DA9DA79F + sample 195: + time = 3900000 + flags = 1 + data = length 13, hash 21B95B7E + sample 196: + time = 3920000 + flags = 1 + data = length 13, hash CD22497B + sample 197: + time = 3940000 + flags = 1 + data = length 13, hash 718BB35D + sample 198: + time = 3960000 + flags = 1 + data = length 13, hash 69ABA6AD + sample 199: + time = 3980000 + flags = 1 + data = length 13, hash BAE19549 + sample 200: + time = 4000000 + flags = 1 + data = length 13, hash 2A792FB3 + sample 201: + time = 4020000 + flags = 1 + data = length 13, hash 71FCD8 + sample 202: + time = 4040000 + flags = 1 + data = length 13, hash 44D2B5B3 + sample 203: + time = 4060000 + flags = 1 + data = length 13, hash 1E87B11B + sample 204: + time = 4080000 + flags = 1 + data = length 13, hash 78CD2C11 + sample 205: + time = 4100000 + flags = 1 + data = length 13, hash 9F198DF0 + sample 206: + time = 4120000 + flags = 1 + data = length 13, hash B291F16A + sample 207: + time = 4140000 + flags = 1 + data = length 13, hash CF820EE0 + sample 208: + time = 4160000 + flags = 1 + data = length 13, hash 4E24F683 + sample 209: + time = 4180000 + flags = 1 + data = length 13, hash 52BCD68F + sample 210: + time = 4200000 + flags = 1 + data = length 13, hash 42588CB0 + sample 211: + time = 4220000 + flags = 1 + data = length 13, hash EBBFECA2 + sample 212: + time = 4240000 + flags = 1 + data = length 13, hash C11050CF + sample 213: + time = 4260000 + flags = 1 + data = length 13, hash 6F738603 + sample 214: + time = 4280000 + flags = 1 + data = length 13, hash DAD06E5 + sample 215: + time = 4300000 + flags = 1 + data = length 13, hash 5B036C64 + sample 216: + time = 4320000 + flags = 1 + data = length 13, hash A58DC12E + sample 217: + time = 4340000 + flags = 1 + data = length 13, hash AC59BA7C +tracksEnded = true diff --git a/library/core/src/test/assets/amr/sample_nb_cbr.amr.1.dump b/library/core/src/test/assets/amr/sample_nb_cbr.amr.1.dump new file mode 100644 index 0000000000..d00ae65c7e --- /dev/null +++ b/library/core/src/test/assets/amr/sample_nb_cbr.amr.1.dump @@ -0,0 +1,614 @@ +seekMap: + isSeekable = true + duration = 4360000 + getPosition(0) = [[timeUs=0, position=6]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = null + containerMimeType = null + sampleMimeType = audio/3gpp + maxInputSize = 61 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 8000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 1898 + sample count = 146 + sample 0: + time = 1440000 + flags = 1 + data = length 13, hash 93BB890A + sample 1: + time = 1460000 + flags = 1 + data = length 13, hash B57DBEC8 + sample 2: + time = 1480000 + flags = 1 + data = length 13, hash 66B0A5B6 + sample 3: + time = 1500000 + flags = 1 + data = length 13, hash D733E0D + sample 4: + time = 1520000 + flags = 1 + data = length 13, hash 80941726 + sample 5: + time = 1540000 + flags = 1 + data = length 13, hash 556ED633 + sample 6: + time = 1560000 + flags = 1 + data = length 13, hash C5EDF4E1 + sample 7: + time = 1580000 + flags = 1 + data = length 13, hash 6B287445 + sample 8: + time = 1600000 + flags = 1 + data = length 13, hash DC97C4A7 + sample 9: + time = 1620000 + flags = 1 + data = length 13, hash DA8CBDF4 + sample 10: + time = 1640000 + flags = 1 + data = length 13, hash 6F60FF77 + sample 11: + time = 1660000 + flags = 1 + data = length 13, hash 3EB22B96 + sample 12: + time = 1680000 + flags = 1 + data = length 13, hash B3C31AF5 + sample 13: + time = 1700000 + flags = 1 + data = length 13, hash 1854AA92 + sample 14: + time = 1720000 + flags = 1 + data = length 13, hash 6488264B + sample 15: + time = 1740000 + flags = 1 + data = length 13, hash 4CC8C5C1 + sample 16: + time = 1760000 + flags = 1 + data = length 13, hash 19CC7523 + sample 17: + time = 1780000 + flags = 1 + data = length 13, hash 9BE7B928 + sample 18: + time = 1800000 + flags = 1 + data = length 13, hash 47EC7CFD + sample 19: + time = 1820000 + flags = 1 + data = length 13, hash EC940120 + sample 20: + time = 1840000 + flags = 1 + data = length 13, hash 73BDA6D0 + sample 21: + time = 1860000 + flags = 1 + data = length 13, hash FACB3314 + sample 22: + time = 1880000 + flags = 1 + data = length 13, hash EC61D13B + sample 23: + time = 1900000 + flags = 1 + data = length 13, hash B28C7B6C + sample 24: + time = 1920000 + flags = 1 + data = length 13, hash B1A4CECD + sample 25: + time = 1940000 + flags = 1 + data = length 13, hash 56D41BA6 + sample 26: + time = 1960000 + flags = 1 + data = length 13, hash 90499F4 + sample 27: + time = 1980000 + flags = 1 + data = length 13, hash 65D9A9D3 + sample 28: + time = 2000000 + flags = 1 + data = length 13, hash D9004CC + sample 29: + time = 2020000 + flags = 1 + data = length 13, hash 4139C6ED + sample 30: + time = 2040000 + flags = 1 + data = length 13, hash C4F8097C + sample 31: + time = 2060000 + flags = 1 + data = length 13, hash 94D424FA + sample 32: + time = 2080000 + flags = 1 + data = length 13, hash C2C6F5FD + sample 33: + time = 2100000 + flags = 1 + data = length 13, hash 15719008 + sample 34: + time = 2120000 + flags = 1 + data = length 13, hash 4F64F524 + sample 35: + time = 2140000 + flags = 1 + data = length 13, hash F9E01C1E + sample 36: + time = 2160000 + flags = 1 + data = length 13, hash 74C4EE74 + sample 37: + time = 2180000 + flags = 1 + data = length 13, hash 7EE7553D + sample 38: + time = 2200000 + flags = 1 + data = length 13, hash 62DE6539 + sample 39: + time = 2220000 + flags = 1 + data = length 13, hash 7F5EC222 + sample 40: + time = 2240000 + flags = 1 + data = length 13, hash 644067F + sample 41: + time = 2260000 + flags = 1 + data = length 13, hash CDF6C9DC + sample 42: + time = 2280000 + flags = 1 + data = length 13, hash 8B5DBC80 + sample 43: + time = 2300000 + flags = 1 + data = length 13, hash AD4BBA03 + sample 44: + time = 2320000 + flags = 1 + data = length 13, hash 7A76340 + sample 45: + time = 2340000 + flags = 1 + data = length 13, hash 3610F5B0 + sample 46: + time = 2360000 + flags = 1 + data = length 13, hash 430BC60B + sample 47: + time = 2380000 + flags = 1 + data = length 13, hash 99CF1CA6 + sample 48: + time = 2400000 + flags = 1 + data = length 13, hash 1331C70B + sample 49: + time = 2420000 + flags = 1 + data = length 13, hash BD76E69D + sample 50: + time = 2440000 + flags = 1 + data = length 13, hash 5DA652AC + sample 51: + time = 2460000 + flags = 1 + data = length 13, hash 3B7BF6CE + sample 52: + time = 2480000 + flags = 1 + data = length 13, hash ABBFD143 + sample 53: + time = 2500000 + flags = 1 + data = length 13, hash E9447166 + sample 54: + time = 2520000 + flags = 1 + data = length 13, hash EC40068C + sample 55: + time = 2540000 + flags = 1 + data = length 13, hash A2869400 + sample 56: + time = 2560000 + flags = 1 + data = length 13, hash C7E0746B + sample 57: + time = 2580000 + flags = 1 + data = length 13, hash 60601BB1 + sample 58: + time = 2600000 + flags = 1 + data = length 13, hash 975AAE9B + sample 59: + time = 2620000 + flags = 1 + data = length 13, hash 8BBC0EB2 + sample 60: + time = 2640000 + flags = 1 + data = length 13, hash 57FB39E5 + sample 61: + time = 2660000 + flags = 1 + data = length 13, hash 4CDCEEDB + sample 62: + time = 2680000 + flags = 1 + data = length 13, hash EA16E256 + sample 63: + time = 2700000 + flags = 1 + data = length 13, hash 287E7D9E + sample 64: + time = 2720000 + flags = 1 + data = length 13, hash 55AB8FB9 + sample 65: + time = 2740000 + flags = 1 + data = length 13, hash 129890EF + sample 66: + time = 2760000 + flags = 1 + data = length 13, hash 90834F57 + sample 67: + time = 2780000 + flags = 1 + data = length 13, hash 5B3228E0 + sample 68: + time = 2800000 + flags = 1 + data = length 13, hash DD19E175 + sample 69: + time = 2820000 + flags = 1 + data = length 13, hash EE7EA342 + sample 70: + time = 2840000 + flags = 1 + data = length 13, hash DB3AF473 + sample 71: + time = 2860000 + flags = 1 + data = length 13, hash 25AEC43F + sample 72: + time = 2880000 + flags = 1 + data = length 13, hash EE9BF97F + sample 73: + time = 2900000 + flags = 1 + data = length 13, hash FFFBE047 + sample 74: + time = 2920000 + flags = 1 + data = length 13, hash BEACFCB0 + sample 75: + time = 2940000 + flags = 1 + data = length 13, hash AEB5096C + sample 76: + time = 2960000 + flags = 1 + data = length 13, hash B0D381B + sample 77: + time = 2980000 + flags = 1 + data = length 13, hash 3D9D5122 + sample 78: + time = 3000000 + flags = 1 + data = length 13, hash 6C1DDB95 + sample 79: + time = 3020000 + flags = 1 + data = length 13, hash ADACADCF + sample 80: + time = 3040000 + flags = 1 + data = length 13, hash 159E321E + sample 81: + time = 3060000 + flags = 1 + data = length 13, hash B1466264 + sample 82: + time = 3080000 + flags = 1 + data = length 13, hash 4DDF7223 + sample 83: + time = 3100000 + flags = 1 + data = length 13, hash C9BDB82A + sample 84: + time = 3120000 + flags = 1 + data = length 13, hash A49B2D9D + sample 85: + time = 3140000 + flags = 1 + data = length 13, hash D645E7E5 + sample 86: + time = 3160000 + flags = 1 + data = length 13, hash 1C4232DC + sample 87: + time = 3180000 + flags = 1 + data = length 13, hash 83078219 + sample 88: + time = 3200000 + flags = 1 + data = length 13, hash D6D8B072 + sample 89: + time = 3220000 + flags = 1 + data = length 13, hash 975DB40 + sample 90: + time = 3240000 + flags = 1 + data = length 13, hash A15FDD05 + sample 91: + time = 3260000 + flags = 1 + data = length 13, hash 4B839E41 + sample 92: + time = 3280000 + flags = 1 + data = length 13, hash 7418F499 + sample 93: + time = 3300000 + flags = 1 + data = length 13, hash 7A4945E4 + sample 94: + time = 3320000 + flags = 1 + data = length 13, hash 6249558C + sample 95: + time = 3340000 + flags = 1 + data = length 13, hash BD4C5BE3 + sample 96: + time = 3360000 + flags = 1 + data = length 13, hash BAB30F1D + sample 97: + time = 3380000 + flags = 1 + data = length 13, hash 1E1C7012 + sample 98: + time = 3400000 + flags = 1 + data = length 13, hash 9A3F8A89 + sample 99: + time = 3420000 + flags = 1 + data = length 13, hash 20BE6D7B + sample 100: + time = 3440000 + flags = 1 + data = length 13, hash CAA0591D + sample 101: + time = 3460000 + flags = 1 + data = length 13, hash 6D554D17 + sample 102: + time = 3480000 + flags = 1 + data = length 13, hash D97C3B31 + sample 103: + time = 3500000 + flags = 1 + data = length 13, hash 75BC5C3 + sample 104: + time = 3520000 + flags = 1 + data = length 13, hash 7BA1784B + sample 105: + time = 3540000 + flags = 1 + data = length 13, hash 1D175D92 + sample 106: + time = 3560000 + flags = 1 + data = length 13, hash ADCA60FD + sample 107: + time = 3580000 + flags = 1 + data = length 13, hash 37018693 + sample 108: + time = 3600000 + flags = 1 + data = length 13, hash 4553606F + sample 109: + time = 3620000 + flags = 1 + data = length 13, hash CF434565 + sample 110: + time = 3640000 + flags = 1 + data = length 13, hash D264D757 + sample 111: + time = 3660000 + flags = 1 + data = length 13, hash 4FB493EF + sample 112: + time = 3680000 + flags = 1 + data = length 13, hash 919F53A + sample 113: + time = 3700000 + flags = 1 + data = length 13, hash C22B009B + sample 114: + time = 3720000 + flags = 1 + data = length 13, hash 5981470 + sample 115: + time = 3740000 + flags = 1 + data = length 13, hash A5D3937C + sample 116: + time = 3760000 + flags = 1 + data = length 13, hash A2504429 + sample 117: + time = 3780000 + flags = 1 + data = length 13, hash AD1B70BE + sample 118: + time = 3800000 + flags = 1 + data = length 13, hash 2E39ED5E + sample 119: + time = 3820000 + flags = 1 + data = length 13, hash 13A8BE8E + sample 120: + time = 3840000 + flags = 1 + data = length 13, hash 1ACD740B + sample 121: + time = 3860000 + flags = 1 + data = length 13, hash 80F38B3 + sample 122: + time = 3880000 + flags = 1 + data = length 13, hash DA9DA79F + sample 123: + time = 3900000 + flags = 1 + data = length 13, hash 21B95B7E + sample 124: + time = 3920000 + flags = 1 + data = length 13, hash CD22497B + sample 125: + time = 3940000 + flags = 1 + data = length 13, hash 718BB35D + sample 126: + time = 3960000 + flags = 1 + data = length 13, hash 69ABA6AD + sample 127: + time = 3980000 + flags = 1 + data = length 13, hash BAE19549 + sample 128: + time = 4000000 + flags = 1 + data = length 13, hash 2A792FB3 + sample 129: + time = 4020000 + flags = 1 + data = length 13, hash 71FCD8 + sample 130: + time = 4040000 + flags = 1 + data = length 13, hash 44D2B5B3 + sample 131: + time = 4060000 + flags = 1 + data = length 13, hash 1E87B11B + sample 132: + time = 4080000 + flags = 1 + data = length 13, hash 78CD2C11 + sample 133: + time = 4100000 + flags = 1 + data = length 13, hash 9F198DF0 + sample 134: + time = 4120000 + flags = 1 + data = length 13, hash B291F16A + sample 135: + time = 4140000 + flags = 1 + data = length 13, hash CF820EE0 + sample 136: + time = 4160000 + flags = 1 + data = length 13, hash 4E24F683 + sample 137: + time = 4180000 + flags = 1 + data = length 13, hash 52BCD68F + sample 138: + time = 4200000 + flags = 1 + data = length 13, hash 42588CB0 + sample 139: + time = 4220000 + flags = 1 + data = length 13, hash EBBFECA2 + sample 140: + time = 4240000 + flags = 1 + data = length 13, hash C11050CF + sample 141: + time = 4260000 + flags = 1 + data = length 13, hash 6F738603 + sample 142: + time = 4280000 + flags = 1 + data = length 13, hash DAD06E5 + sample 143: + time = 4300000 + flags = 1 + data = length 13, hash 5B036C64 + sample 144: + time = 4320000 + flags = 1 + data = length 13, hash A58DC12E + sample 145: + time = 4340000 + flags = 1 + data = length 13, hash AC59BA7C +tracksEnded = true diff --git a/library/core/src/test/assets/amr/sample_nb_cbr.amr.2.dump b/library/core/src/test/assets/amr/sample_nb_cbr.amr.2.dump new file mode 100644 index 0000000000..f68b6df3a3 --- /dev/null +++ b/library/core/src/test/assets/amr/sample_nb_cbr.amr.2.dump @@ -0,0 +1,322 @@ +seekMap: + isSeekable = true + duration = 4360000 + getPosition(0) = [[timeUs=0, position=6]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = null + containerMimeType = null + sampleMimeType = audio/3gpp + maxInputSize = 61 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 8000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 949 + sample count = 73 + sample 0: + time = 2900000 + flags = 1 + data = length 13, hash FFFBE047 + sample 1: + time = 2920000 + flags = 1 + data = length 13, hash BEACFCB0 + sample 2: + time = 2940000 + flags = 1 + data = length 13, hash AEB5096C + sample 3: + time = 2960000 + flags = 1 + data = length 13, hash B0D381B + sample 4: + time = 2980000 + flags = 1 + data = length 13, hash 3D9D5122 + sample 5: + time = 3000000 + flags = 1 + data = length 13, hash 6C1DDB95 + sample 6: + time = 3020000 + flags = 1 + data = length 13, hash ADACADCF + sample 7: + time = 3040000 + flags = 1 + data = length 13, hash 159E321E + sample 8: + time = 3060000 + flags = 1 + data = length 13, hash B1466264 + sample 9: + time = 3080000 + flags = 1 + data = length 13, hash 4DDF7223 + sample 10: + time = 3100000 + flags = 1 + data = length 13, hash C9BDB82A + sample 11: + time = 3120000 + flags = 1 + data = length 13, hash A49B2D9D + sample 12: + time = 3140000 + flags = 1 + data = length 13, hash D645E7E5 + sample 13: + time = 3160000 + flags = 1 + data = length 13, hash 1C4232DC + sample 14: + time = 3180000 + flags = 1 + data = length 13, hash 83078219 + sample 15: + time = 3200000 + flags = 1 + data = length 13, hash D6D8B072 + sample 16: + time = 3220000 + flags = 1 + data = length 13, hash 975DB40 + sample 17: + time = 3240000 + flags = 1 + data = length 13, hash A15FDD05 + sample 18: + time = 3260000 + flags = 1 + data = length 13, hash 4B839E41 + sample 19: + time = 3280000 + flags = 1 + data = length 13, hash 7418F499 + sample 20: + time = 3300000 + flags = 1 + data = length 13, hash 7A4945E4 + sample 21: + time = 3320000 + flags = 1 + data = length 13, hash 6249558C + sample 22: + time = 3340000 + flags = 1 + data = length 13, hash BD4C5BE3 + sample 23: + time = 3360000 + flags = 1 + data = length 13, hash BAB30F1D + sample 24: + time = 3380000 + flags = 1 + data = length 13, hash 1E1C7012 + sample 25: + time = 3400000 + flags = 1 + data = length 13, hash 9A3F8A89 + sample 26: + time = 3420000 + flags = 1 + data = length 13, hash 20BE6D7B + sample 27: + time = 3440000 + flags = 1 + data = length 13, hash CAA0591D + sample 28: + time = 3460000 + flags = 1 + data = length 13, hash 6D554D17 + sample 29: + time = 3480000 + flags = 1 + data = length 13, hash D97C3B31 + sample 30: + time = 3500000 + flags = 1 + data = length 13, hash 75BC5C3 + sample 31: + time = 3520000 + flags = 1 + data = length 13, hash 7BA1784B + sample 32: + time = 3540000 + flags = 1 + data = length 13, hash 1D175D92 + sample 33: + time = 3560000 + flags = 1 + data = length 13, hash ADCA60FD + sample 34: + time = 3580000 + flags = 1 + data = length 13, hash 37018693 + sample 35: + time = 3600000 + flags = 1 + data = length 13, hash 4553606F + sample 36: + time = 3620000 + flags = 1 + data = length 13, hash CF434565 + sample 37: + time = 3640000 + flags = 1 + data = length 13, hash D264D757 + sample 38: + time = 3660000 + flags = 1 + data = length 13, hash 4FB493EF + sample 39: + time = 3680000 + flags = 1 + data = length 13, hash 919F53A + sample 40: + time = 3700000 + flags = 1 + data = length 13, hash C22B009B + sample 41: + time = 3720000 + flags = 1 + data = length 13, hash 5981470 + sample 42: + time = 3740000 + flags = 1 + data = length 13, hash A5D3937C + sample 43: + time = 3760000 + flags = 1 + data = length 13, hash A2504429 + sample 44: + time = 3780000 + flags = 1 + data = length 13, hash AD1B70BE + sample 45: + time = 3800000 + flags = 1 + data = length 13, hash 2E39ED5E + sample 46: + time = 3820000 + flags = 1 + data = length 13, hash 13A8BE8E + sample 47: + time = 3840000 + flags = 1 + data = length 13, hash 1ACD740B + sample 48: + time = 3860000 + flags = 1 + data = length 13, hash 80F38B3 + sample 49: + time = 3880000 + flags = 1 + data = length 13, hash DA9DA79F + sample 50: + time = 3900000 + flags = 1 + data = length 13, hash 21B95B7E + sample 51: + time = 3920000 + flags = 1 + data = length 13, hash CD22497B + sample 52: + time = 3940000 + flags = 1 + data = length 13, hash 718BB35D + sample 53: + time = 3960000 + flags = 1 + data = length 13, hash 69ABA6AD + sample 54: + time = 3980000 + flags = 1 + data = length 13, hash BAE19549 + sample 55: + time = 4000000 + flags = 1 + data = length 13, hash 2A792FB3 + sample 56: + time = 4020000 + flags = 1 + data = length 13, hash 71FCD8 + sample 57: + time = 4040000 + flags = 1 + data = length 13, hash 44D2B5B3 + sample 58: + time = 4060000 + flags = 1 + data = length 13, hash 1E87B11B + sample 59: + time = 4080000 + flags = 1 + data = length 13, hash 78CD2C11 + sample 60: + time = 4100000 + flags = 1 + data = length 13, hash 9F198DF0 + sample 61: + time = 4120000 + flags = 1 + data = length 13, hash B291F16A + sample 62: + time = 4140000 + flags = 1 + data = length 13, hash CF820EE0 + sample 63: + time = 4160000 + flags = 1 + data = length 13, hash 4E24F683 + sample 64: + time = 4180000 + flags = 1 + data = length 13, hash 52BCD68F + sample 65: + time = 4200000 + flags = 1 + data = length 13, hash 42588CB0 + sample 66: + time = 4220000 + flags = 1 + data = length 13, hash EBBFECA2 + sample 67: + time = 4240000 + flags = 1 + data = length 13, hash C11050CF + sample 68: + time = 4260000 + flags = 1 + data = length 13, hash 6F738603 + sample 69: + time = 4280000 + flags = 1 + data = length 13, hash DAD06E5 + sample 70: + time = 4300000 + flags = 1 + data = length 13, hash 5B036C64 + sample 71: + time = 4320000 + flags = 1 + data = length 13, hash A58DC12E + sample 72: + time = 4340000 + flags = 1 + data = length 13, hash AC59BA7C +tracksEnded = true diff --git a/library/core/src/test/assets/amr/sample_nb_cbr.amr.3.dump b/library/core/src/test/assets/amr/sample_nb_cbr.amr.3.dump new file mode 100644 index 0000000000..da907e004f --- /dev/null +++ b/library/core/src/test/assets/amr/sample_nb_cbr.amr.3.dump @@ -0,0 +1,34 @@ +seekMap: + isSeekable = true + duration = 4360000 + getPosition(0) = [[timeUs=0, position=6]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = null + containerMimeType = null + sampleMimeType = audio/3gpp + maxInputSize = 61 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 8000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 13 + sample count = 1 + sample 0: + time = 4340000 + flags = 1 + data = length 13, hash AC59BA7C +tracksEnded = true diff --git a/library/core/src/test/assets/amr/sample_nb_cbr.amr.unklen.dump b/library/core/src/test/assets/amr/sample_nb_cbr.amr.unklen.dump new file mode 100644 index 0000000000..e0dec9c62c --- /dev/null +++ b/library/core/src/test/assets/amr/sample_nb_cbr.amr.unklen.dump @@ -0,0 +1,902 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = null + containerMimeType = null + sampleMimeType = audio/3gpp + maxInputSize = 61 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 8000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 2834 + sample count = 218 + sample 0: + time = 0 + flags = 1 + data = length 13, hash 371B046C + sample 1: + time = 20000 + flags = 1 + data = length 13, hash CE30BF5B + sample 2: + time = 40000 + flags = 1 + data = length 13, hash 19A59975 + sample 3: + time = 60000 + flags = 1 + data = length 13, hash 4879773C + sample 4: + time = 80000 + flags = 1 + data = length 13, hash E8F83019 + sample 5: + time = 100000 + flags = 1 + data = length 13, hash D265CDC9 + sample 6: + time = 120000 + flags = 1 + data = length 13, hash 91653DAA + sample 7: + time = 140000 + flags = 1 + data = length 13, hash C79456F6 + sample 8: + time = 160000 + flags = 1 + data = length 13, hash CDDC4422 + sample 9: + time = 180000 + flags = 1 + data = length 13, hash D9ED3AF1 + sample 10: + time = 200000 + flags = 1 + data = length 13, hash BAB75A33 + sample 11: + time = 220000 + flags = 1 + data = length 13, hash 2221B4FF + sample 12: + time = 240000 + flags = 1 + data = length 13, hash 96400A0B + sample 13: + time = 260000 + flags = 1 + data = length 13, hash 582E6FB + sample 14: + time = 280000 + flags = 1 + data = length 13, hash C4E878E5 + sample 15: + time = 300000 + flags = 1 + data = length 13, hash C849A1BD + sample 16: + time = 320000 + flags = 1 + data = length 13, hash CFA8A9ED + sample 17: + time = 340000 + flags = 1 + data = length 13, hash 70CA4907 + sample 18: + time = 360000 + flags = 1 + data = length 13, hash B47D4454 + sample 19: + time = 380000 + flags = 1 + data = length 13, hash 282998C1 + sample 20: + time = 400000 + flags = 1 + data = length 13, hash 3F3F7A65 + sample 21: + time = 420000 + flags = 1 + data = length 13, hash CC2EAB58 + sample 22: + time = 440000 + flags = 1 + data = length 13, hash 279EF712 + sample 23: + time = 460000 + flags = 1 + data = length 13, hash AA2F4B29 + sample 24: + time = 480000 + flags = 1 + data = length 13, hash F6F658C4 + sample 25: + time = 500000 + flags = 1 + data = length 13, hash D7DEBD17 + sample 26: + time = 520000 + flags = 1 + data = length 13, hash 6DAB9A17 + sample 27: + time = 540000 + flags = 1 + data = length 13, hash 6ECE1571 + sample 28: + time = 560000 + flags = 1 + data = length 13, hash B3D0507F + sample 29: + time = 580000 + flags = 1 + data = length 13, hash 21E356B9 + sample 30: + time = 600000 + flags = 1 + data = length 13, hash 410EA12 + sample 31: + time = 620000 + flags = 1 + data = length 13, hash 533895A8 + sample 32: + time = 640000 + flags = 1 + data = length 13, hash C61B3E5A + sample 33: + time = 660000 + flags = 1 + data = length 13, hash 982170E6 + sample 34: + time = 680000 + flags = 1 + data = length 13, hash 7A0468C5 + sample 35: + time = 700000 + flags = 1 + data = length 13, hash 9C85EAA7 + sample 36: + time = 720000 + flags = 1 + data = length 13, hash B6B341B6 + sample 37: + time = 740000 + flags = 1 + data = length 13, hash 6937532E + sample 38: + time = 760000 + flags = 1 + data = length 13, hash 8CF2A3A0 + sample 39: + time = 780000 + flags = 1 + data = length 13, hash D2682AC6 + sample 40: + time = 800000 + flags = 1 + data = length 13, hash BBC5710F + sample 41: + time = 820000 + flags = 1 + data = length 13, hash 59080B6C + sample 42: + time = 840000 + flags = 1 + data = length 13, hash E4118291 + sample 43: + time = 860000 + flags = 1 + data = length 13, hash A1E5B296 + sample 44: + time = 880000 + flags = 1 + data = length 13, hash D7B8F95B + sample 45: + time = 900000 + flags = 1 + data = length 13, hash CC839BE1 + sample 46: + time = 920000 + flags = 1 + data = length 13, hash D459DFCE + sample 47: + time = 940000 + flags = 1 + data = length 13, hash D6AD19EC + sample 48: + time = 960000 + flags = 1 + data = length 13, hash D05E373D + sample 49: + time = 980000 + flags = 1 + data = length 13, hash 6A4460C7 + sample 50: + time = 1000000 + flags = 1 + data = length 13, hash C9A0D93F + sample 51: + time = 1020000 + flags = 1 + data = length 13, hash 3FA819E7 + sample 52: + time = 1040000 + flags = 1 + data = length 13, hash 1D3CBDFC + sample 53: + time = 1060000 + flags = 1 + data = length 13, hash 8BBBB403 + sample 54: + time = 1080000 + flags = 1 + data = length 13, hash 21B4A0F9 + sample 55: + time = 1100000 + flags = 1 + data = length 13, hash C0F921D1 + sample 56: + time = 1120000 + flags = 1 + data = length 13, hash 5D812AAB + sample 57: + time = 1140000 + flags = 1 + data = length 13, hash 50C9F3F8 + sample 58: + time = 1160000 + flags = 1 + data = length 13, hash 5C2BB5D1 + sample 59: + time = 1180000 + flags = 1 + data = length 13, hash 6BF9BEA5 + sample 60: + time = 1200000 + flags = 1 + data = length 13, hash 2738C1E6 + sample 61: + time = 1220000 + flags = 1 + data = length 13, hash 5FC288A6 + sample 62: + time = 1240000 + flags = 1 + data = length 13, hash 7E8E442A + sample 63: + time = 1260000 + flags = 1 + data = length 13, hash AEAA2BBA + sample 64: + time = 1280000 + flags = 1 + data = length 13, hash 4E2ACD2F + sample 65: + time = 1300000 + flags = 1 + data = length 13, hash D6C90ACF + sample 66: + time = 1320000 + flags = 1 + data = length 13, hash 6FD8A944 + sample 67: + time = 1340000 + flags = 1 + data = length 13, hash A835BBF9 + sample 68: + time = 1360000 + flags = 1 + data = length 13, hash F7713830 + sample 69: + time = 1380000 + flags = 1 + data = length 13, hash 3AA966E5 + sample 70: + time = 1400000 + flags = 1 + data = length 13, hash F939E829 + sample 71: + time = 1420000 + flags = 1 + data = length 13, hash 7676DE49 + sample 72: + time = 1440000 + flags = 1 + data = length 13, hash 93BB890A + sample 73: + time = 1460000 + flags = 1 + data = length 13, hash B57DBEC8 + sample 74: + time = 1480000 + flags = 1 + data = length 13, hash 66B0A5B6 + sample 75: + time = 1500000 + flags = 1 + data = length 13, hash D733E0D + sample 76: + time = 1520000 + flags = 1 + data = length 13, hash 80941726 + sample 77: + time = 1540000 + flags = 1 + data = length 13, hash 556ED633 + sample 78: + time = 1560000 + flags = 1 + data = length 13, hash C5EDF4E1 + sample 79: + time = 1580000 + flags = 1 + data = length 13, hash 6B287445 + sample 80: + time = 1600000 + flags = 1 + data = length 13, hash DC97C4A7 + sample 81: + time = 1620000 + flags = 1 + data = length 13, hash DA8CBDF4 + sample 82: + time = 1640000 + flags = 1 + data = length 13, hash 6F60FF77 + sample 83: + time = 1660000 + flags = 1 + data = length 13, hash 3EB22B96 + sample 84: + time = 1680000 + flags = 1 + data = length 13, hash B3C31AF5 + sample 85: + time = 1700000 + flags = 1 + data = length 13, hash 1854AA92 + sample 86: + time = 1720000 + flags = 1 + data = length 13, hash 6488264B + sample 87: + time = 1740000 + flags = 1 + data = length 13, hash 4CC8C5C1 + sample 88: + time = 1760000 + flags = 1 + data = length 13, hash 19CC7523 + sample 89: + time = 1780000 + flags = 1 + data = length 13, hash 9BE7B928 + sample 90: + time = 1800000 + flags = 1 + data = length 13, hash 47EC7CFD + sample 91: + time = 1820000 + flags = 1 + data = length 13, hash EC940120 + sample 92: + time = 1840000 + flags = 1 + data = length 13, hash 73BDA6D0 + sample 93: + time = 1860000 + flags = 1 + data = length 13, hash FACB3314 + sample 94: + time = 1880000 + flags = 1 + data = length 13, hash EC61D13B + sample 95: + time = 1900000 + flags = 1 + data = length 13, hash B28C7B6C + sample 96: + time = 1920000 + flags = 1 + data = length 13, hash B1A4CECD + sample 97: + time = 1940000 + flags = 1 + data = length 13, hash 56D41BA6 + sample 98: + time = 1960000 + flags = 1 + data = length 13, hash 90499F4 + sample 99: + time = 1980000 + flags = 1 + data = length 13, hash 65D9A9D3 + sample 100: + time = 2000000 + flags = 1 + data = length 13, hash D9004CC + sample 101: + time = 2020000 + flags = 1 + data = length 13, hash 4139C6ED + sample 102: + time = 2040000 + flags = 1 + data = length 13, hash C4F8097C + sample 103: + time = 2060000 + flags = 1 + data = length 13, hash 94D424FA + sample 104: + time = 2080000 + flags = 1 + data = length 13, hash C2C6F5FD + sample 105: + time = 2100000 + flags = 1 + data = length 13, hash 15719008 + sample 106: + time = 2120000 + flags = 1 + data = length 13, hash 4F64F524 + sample 107: + time = 2140000 + flags = 1 + data = length 13, hash F9E01C1E + sample 108: + time = 2160000 + flags = 1 + data = length 13, hash 74C4EE74 + sample 109: + time = 2180000 + flags = 1 + data = length 13, hash 7EE7553D + sample 110: + time = 2200000 + flags = 1 + data = length 13, hash 62DE6539 + sample 111: + time = 2220000 + flags = 1 + data = length 13, hash 7F5EC222 + sample 112: + time = 2240000 + flags = 1 + data = length 13, hash 644067F + sample 113: + time = 2260000 + flags = 1 + data = length 13, hash CDF6C9DC + sample 114: + time = 2280000 + flags = 1 + data = length 13, hash 8B5DBC80 + sample 115: + time = 2300000 + flags = 1 + data = length 13, hash AD4BBA03 + sample 116: + time = 2320000 + flags = 1 + data = length 13, hash 7A76340 + sample 117: + time = 2340000 + flags = 1 + data = length 13, hash 3610F5B0 + sample 118: + time = 2360000 + flags = 1 + data = length 13, hash 430BC60B + sample 119: + time = 2380000 + flags = 1 + data = length 13, hash 99CF1CA6 + sample 120: + time = 2400000 + flags = 1 + data = length 13, hash 1331C70B + sample 121: + time = 2420000 + flags = 1 + data = length 13, hash BD76E69D + sample 122: + time = 2440000 + flags = 1 + data = length 13, hash 5DA652AC + sample 123: + time = 2460000 + flags = 1 + data = length 13, hash 3B7BF6CE + sample 124: + time = 2480000 + flags = 1 + data = length 13, hash ABBFD143 + sample 125: + time = 2500000 + flags = 1 + data = length 13, hash E9447166 + sample 126: + time = 2520000 + flags = 1 + data = length 13, hash EC40068C + sample 127: + time = 2540000 + flags = 1 + data = length 13, hash A2869400 + sample 128: + time = 2560000 + flags = 1 + data = length 13, hash C7E0746B + sample 129: + time = 2580000 + flags = 1 + data = length 13, hash 60601BB1 + sample 130: + time = 2600000 + flags = 1 + data = length 13, hash 975AAE9B + sample 131: + time = 2620000 + flags = 1 + data = length 13, hash 8BBC0EB2 + sample 132: + time = 2640000 + flags = 1 + data = length 13, hash 57FB39E5 + sample 133: + time = 2660000 + flags = 1 + data = length 13, hash 4CDCEEDB + sample 134: + time = 2680000 + flags = 1 + data = length 13, hash EA16E256 + sample 135: + time = 2700000 + flags = 1 + data = length 13, hash 287E7D9E + sample 136: + time = 2720000 + flags = 1 + data = length 13, hash 55AB8FB9 + sample 137: + time = 2740000 + flags = 1 + data = length 13, hash 129890EF + sample 138: + time = 2760000 + flags = 1 + data = length 13, hash 90834F57 + sample 139: + time = 2780000 + flags = 1 + data = length 13, hash 5B3228E0 + sample 140: + time = 2800000 + flags = 1 + data = length 13, hash DD19E175 + sample 141: + time = 2820000 + flags = 1 + data = length 13, hash EE7EA342 + sample 142: + time = 2840000 + flags = 1 + data = length 13, hash DB3AF473 + sample 143: + time = 2860000 + flags = 1 + data = length 13, hash 25AEC43F + sample 144: + time = 2880000 + flags = 1 + data = length 13, hash EE9BF97F + sample 145: + time = 2900000 + flags = 1 + data = length 13, hash FFFBE047 + sample 146: + time = 2920000 + flags = 1 + data = length 13, hash BEACFCB0 + sample 147: + time = 2940000 + flags = 1 + data = length 13, hash AEB5096C + sample 148: + time = 2960000 + flags = 1 + data = length 13, hash B0D381B + sample 149: + time = 2980000 + flags = 1 + data = length 13, hash 3D9D5122 + sample 150: + time = 3000000 + flags = 1 + data = length 13, hash 6C1DDB95 + sample 151: + time = 3020000 + flags = 1 + data = length 13, hash ADACADCF + sample 152: + time = 3040000 + flags = 1 + data = length 13, hash 159E321E + sample 153: + time = 3060000 + flags = 1 + data = length 13, hash B1466264 + sample 154: + time = 3080000 + flags = 1 + data = length 13, hash 4DDF7223 + sample 155: + time = 3100000 + flags = 1 + data = length 13, hash C9BDB82A + sample 156: + time = 3120000 + flags = 1 + data = length 13, hash A49B2D9D + sample 157: + time = 3140000 + flags = 1 + data = length 13, hash D645E7E5 + sample 158: + time = 3160000 + flags = 1 + data = length 13, hash 1C4232DC + sample 159: + time = 3180000 + flags = 1 + data = length 13, hash 83078219 + sample 160: + time = 3200000 + flags = 1 + data = length 13, hash D6D8B072 + sample 161: + time = 3220000 + flags = 1 + data = length 13, hash 975DB40 + sample 162: + time = 3240000 + flags = 1 + data = length 13, hash A15FDD05 + sample 163: + time = 3260000 + flags = 1 + data = length 13, hash 4B839E41 + sample 164: + time = 3280000 + flags = 1 + data = length 13, hash 7418F499 + sample 165: + time = 3300000 + flags = 1 + data = length 13, hash 7A4945E4 + sample 166: + time = 3320000 + flags = 1 + data = length 13, hash 6249558C + sample 167: + time = 3340000 + flags = 1 + data = length 13, hash BD4C5BE3 + sample 168: + time = 3360000 + flags = 1 + data = length 13, hash BAB30F1D + sample 169: + time = 3380000 + flags = 1 + data = length 13, hash 1E1C7012 + sample 170: + time = 3400000 + flags = 1 + data = length 13, hash 9A3F8A89 + sample 171: + time = 3420000 + flags = 1 + data = length 13, hash 20BE6D7B + sample 172: + time = 3440000 + flags = 1 + data = length 13, hash CAA0591D + sample 173: + time = 3460000 + flags = 1 + data = length 13, hash 6D554D17 + sample 174: + time = 3480000 + flags = 1 + data = length 13, hash D97C3B31 + sample 175: + time = 3500000 + flags = 1 + data = length 13, hash 75BC5C3 + sample 176: + time = 3520000 + flags = 1 + data = length 13, hash 7BA1784B + sample 177: + time = 3540000 + flags = 1 + data = length 13, hash 1D175D92 + sample 178: + time = 3560000 + flags = 1 + data = length 13, hash ADCA60FD + sample 179: + time = 3580000 + flags = 1 + data = length 13, hash 37018693 + sample 180: + time = 3600000 + flags = 1 + data = length 13, hash 4553606F + sample 181: + time = 3620000 + flags = 1 + data = length 13, hash CF434565 + sample 182: + time = 3640000 + flags = 1 + data = length 13, hash D264D757 + sample 183: + time = 3660000 + flags = 1 + data = length 13, hash 4FB493EF + sample 184: + time = 3680000 + flags = 1 + data = length 13, hash 919F53A + sample 185: + time = 3700000 + flags = 1 + data = length 13, hash C22B009B + sample 186: + time = 3720000 + flags = 1 + data = length 13, hash 5981470 + sample 187: + time = 3740000 + flags = 1 + data = length 13, hash A5D3937C + sample 188: + time = 3760000 + flags = 1 + data = length 13, hash A2504429 + sample 189: + time = 3780000 + flags = 1 + data = length 13, hash AD1B70BE + sample 190: + time = 3800000 + flags = 1 + data = length 13, hash 2E39ED5E + sample 191: + time = 3820000 + flags = 1 + data = length 13, hash 13A8BE8E + sample 192: + time = 3840000 + flags = 1 + data = length 13, hash 1ACD740B + sample 193: + time = 3860000 + flags = 1 + data = length 13, hash 80F38B3 + sample 194: + time = 3880000 + flags = 1 + data = length 13, hash DA9DA79F + sample 195: + time = 3900000 + flags = 1 + data = length 13, hash 21B95B7E + sample 196: + time = 3920000 + flags = 1 + data = length 13, hash CD22497B + sample 197: + time = 3940000 + flags = 1 + data = length 13, hash 718BB35D + sample 198: + time = 3960000 + flags = 1 + data = length 13, hash 69ABA6AD + sample 199: + time = 3980000 + flags = 1 + data = length 13, hash BAE19549 + sample 200: + time = 4000000 + flags = 1 + data = length 13, hash 2A792FB3 + sample 201: + time = 4020000 + flags = 1 + data = length 13, hash 71FCD8 + sample 202: + time = 4040000 + flags = 1 + data = length 13, hash 44D2B5B3 + sample 203: + time = 4060000 + flags = 1 + data = length 13, hash 1E87B11B + sample 204: + time = 4080000 + flags = 1 + data = length 13, hash 78CD2C11 + sample 205: + time = 4100000 + flags = 1 + data = length 13, hash 9F198DF0 + sample 206: + time = 4120000 + flags = 1 + data = length 13, hash B291F16A + sample 207: + time = 4140000 + flags = 1 + data = length 13, hash CF820EE0 + sample 208: + time = 4160000 + flags = 1 + data = length 13, hash 4E24F683 + sample 209: + time = 4180000 + flags = 1 + data = length 13, hash 52BCD68F + sample 210: + time = 4200000 + flags = 1 + data = length 13, hash 42588CB0 + sample 211: + time = 4220000 + flags = 1 + data = length 13, hash EBBFECA2 + sample 212: + time = 4240000 + flags = 1 + data = length 13, hash C11050CF + sample 213: + time = 4260000 + flags = 1 + data = length 13, hash 6F738603 + sample 214: + time = 4280000 + flags = 1 + data = length 13, hash DAD06E5 + sample 215: + time = 4300000 + flags = 1 + data = length 13, hash 5B036C64 + sample 216: + time = 4320000 + flags = 1 + data = length 13, hash A58DC12E + sample 217: + time = 4340000 + flags = 1 + data = length 13, hash AC59BA7C +tracksEnded = true diff --git a/library/core/src/test/assets/amr/sample_wb_cbr.amr b/library/core/src/test/assets/amr/sample_wb_cbr.amr new file mode 100644 index 0000000000..14b85b553c Binary files /dev/null and b/library/core/src/test/assets/amr/sample_wb_cbr.amr differ diff --git a/library/core/src/test/assets/amr/sample_wb_cbr.amr.0.dump b/library/core/src/test/assets/amr/sample_wb_cbr.amr.0.dump new file mode 100644 index 0000000000..c987c6e357 --- /dev/null +++ b/library/core/src/test/assets/amr/sample_wb_cbr.amr.0.dump @@ -0,0 +1,706 @@ +seekMap: + isSeekable = true + duration = 3380000 + getPosition(0) = [[timeUs=0, position=9]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = null + containerMimeType = null + sampleMimeType = audio/amr-wb + maxInputSize = 61 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 16000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 4056 + sample count = 169 + sample 0: + time = 0 + flags = 1 + data = length 24, hash C3025798 + sample 1: + time = 20000 + flags = 1 + data = length 24, hash 39CABAE9 + sample 2: + time = 40000 + flags = 1 + data = length 24, hash 2752F470 + sample 3: + time = 60000 + flags = 1 + data = length 24, hash 394F76F6 + sample 4: + time = 80000 + flags = 1 + data = length 24, hash FF9EEF + sample 5: + time = 100000 + flags = 1 + data = length 24, hash 54ECB1B4 + sample 6: + time = 120000 + flags = 1 + data = length 24, hash 6D7A3A5F + sample 7: + time = 140000 + flags = 1 + data = length 24, hash 684CD144 + sample 8: + time = 160000 + flags = 1 + data = length 24, hash 87B7D176 + sample 9: + time = 180000 + flags = 1 + data = length 24, hash 4C02F9A5 + sample 10: + time = 200000 + flags = 1 + data = length 24, hash B4154108 + sample 11: + time = 220000 + flags = 1 + data = length 24, hash 4448F477 + sample 12: + time = 240000 + flags = 1 + data = length 24, hash 755A4939 + sample 13: + time = 260000 + flags = 1 + data = length 24, hash 8C8BC6C3 + sample 14: + time = 280000 + flags = 1 + data = length 24, hash BC37F63F + sample 15: + time = 300000 + flags = 1 + data = length 24, hash 3352C43C + sample 16: + time = 320000 + flags = 1 + data = length 24, hash 7998E1F2 + sample 17: + time = 340000 + flags = 1 + data = length 24, hash A8ECBEFC + sample 18: + time = 360000 + flags = 1 + data = length 24, hash 944AC118 + sample 19: + time = 380000 + flags = 1 + data = length 24, hash FD2C8E1F + sample 20: + time = 400000 + flags = 1 + data = length 24, hash B3D867AF + sample 21: + time = 420000 + flags = 1 + data = length 24, hash 3DC6E592 + sample 22: + time = 440000 + flags = 1 + data = length 24, hash 32B276CD + sample 23: + time = 460000 + flags = 1 + data = length 24, hash 5488AEF3 + sample 24: + time = 480000 + flags = 1 + data = length 24, hash 7A4D516 + sample 25: + time = 500000 + flags = 1 + data = length 24, hash 570AE83F + sample 26: + time = 520000 + flags = 1 + data = length 24, hash E5CB3477 + sample 27: + time = 540000 + flags = 1 + data = length 24, hash E04C00E4 + sample 28: + time = 560000 + flags = 1 + data = length 24, hash 21B7C97 + sample 29: + time = 580000 + flags = 1 + data = length 24, hash 1633F470 + sample 30: + time = 600000 + flags = 1 + data = length 24, hash 28D65CA6 + sample 31: + time = 620000 + flags = 1 + data = length 24, hash CC6A675C + sample 32: + time = 640000 + flags = 1 + data = length 24, hash 4C91080A + sample 33: + time = 660000 + flags = 1 + data = length 24, hash F6482FB5 + sample 34: + time = 680000 + flags = 1 + data = length 24, hash 2C76F48C + sample 35: + time = 700000 + flags = 1 + data = length 24, hash 6E3B0D72 + sample 36: + time = 720000 + flags = 1 + data = length 24, hash 799AA003 + sample 37: + time = 740000 + flags = 1 + data = length 24, hash DFC0BA81 + sample 38: + time = 760000 + flags = 1 + data = length 24, hash CBDF3826 + sample 39: + time = 780000 + flags = 1 + data = length 24, hash 16862B75 + sample 40: + time = 800000 + flags = 1 + data = length 24, hash 865A828E + sample 41: + time = 820000 + flags = 1 + data = length 24, hash 336BBDC9 + sample 42: + time = 840000 + flags = 1 + data = length 24, hash 6CFC6C34 + sample 43: + time = 860000 + flags = 1 + data = length 24, hash 32C8CD46 + sample 44: + time = 880000 + flags = 1 + data = length 24, hash 9FE11C4C + sample 45: + time = 900000 + flags = 1 + data = length 24, hash AA5A12B7 + sample 46: + time = 920000 + flags = 1 + data = length 24, hash AA0F4A4D + sample 47: + time = 940000 + flags = 1 + data = length 24, hash 34415484 + sample 48: + time = 960000 + flags = 1 + data = length 24, hash 5018928E + sample 49: + time = 980000 + flags = 1 + data = length 24, hash 4A04D162 + sample 50: + time = 1000000 + flags = 1 + data = length 24, hash 4C70F9F0 + sample 51: + time = 1020000 + flags = 1 + data = length 24, hash 99EF3168 + sample 52: + time = 1040000 + flags = 1 + data = length 24, hash C600DAF + sample 53: + time = 1060000 + flags = 1 + data = length 24, hash FDBB192E + sample 54: + time = 1080000 + flags = 1 + data = length 24, hash 99096A48 + sample 55: + time = 1100000 + flags = 1 + data = length 24, hash D793F88B + sample 56: + time = 1120000 + flags = 1 + data = length 24, hash EEB921BD + sample 57: + time = 1140000 + flags = 1 + data = length 24, hash 8B941A4C + sample 58: + time = 1160000 + flags = 1 + data = length 24, hash ED5F5FEE + sample 59: + time = 1180000 + flags = 1 + data = length 24, hash A588E0BB + sample 60: + time = 1200000 + flags = 1 + data = length 24, hash 588CBC01 + sample 61: + time = 1220000 + flags = 1 + data = length 24, hash DE22266C + sample 62: + time = 1240000 + flags = 1 + data = length 24, hash 921B6E5C + sample 63: + time = 1260000 + flags = 1 + data = length 24, hash EC11F041 + sample 64: + time = 1280000 + flags = 1 + data = length 24, hash 5BA9E0A3 + sample 65: + time = 1300000 + flags = 1 + data = length 24, hash DB6D52F3 + sample 66: + time = 1320000 + flags = 1 + data = length 24, hash 8EEBE525 + sample 67: + time = 1340000 + flags = 1 + data = length 24, hash 47A742AE + sample 68: + time = 1360000 + flags = 1 + data = length 24, hash E93F1E03 + sample 69: + time = 1380000 + flags = 1 + data = length 24, hash 3251F57C + sample 70: + time = 1400000 + flags = 1 + data = length 24, hash 3EDBBBDD + sample 71: + time = 1420000 + flags = 1 + data = length 24, hash 2E98465A + sample 72: + time = 1440000 + flags = 1 + data = length 24, hash A09EA52E + sample 73: + time = 1460000 + flags = 1 + data = length 24, hash A2A86FA6 + sample 74: + time = 1480000 + flags = 1 + data = length 24, hash 71DCD51C + sample 75: + time = 1500000 + flags = 1 + data = length 24, hash 2B02DEE1 + sample 76: + time = 1520000 + flags = 1 + data = length 24, hash 7A725192 + sample 77: + time = 1540000 + flags = 1 + data = length 24, hash 929AD483 + sample 78: + time = 1560000 + flags = 1 + data = length 24, hash 68440BF5 + sample 79: + time = 1580000 + flags = 1 + data = length 24, hash 5BD41AD6 + sample 80: + time = 1600000 + flags = 1 + data = length 24, hash 91A381 + sample 81: + time = 1620000 + flags = 1 + data = length 24, hash 8010C408 + sample 82: + time = 1640000 + flags = 1 + data = length 24, hash 482274BE + sample 83: + time = 1660000 + flags = 1 + data = length 24, hash D7DB8BCC + sample 84: + time = 1680000 + flags = 1 + data = length 24, hash 680BD9DD + sample 85: + time = 1700000 + flags = 1 + data = length 24, hash E313577C + sample 86: + time = 1720000 + flags = 1 + data = length 24, hash 9C10B0CD + sample 87: + time = 1740000 + flags = 1 + data = length 24, hash 2D90AC02 + sample 88: + time = 1760000 + flags = 1 + data = length 24, hash 64E8C245 + sample 89: + time = 1780000 + flags = 1 + data = length 24, hash 3954AC1B + sample 90: + time = 1800000 + flags = 1 + data = length 24, hash ACB8999F + sample 91: + time = 1820000 + flags = 1 + data = length 24, hash 43AE3957 + sample 92: + time = 1840000 + flags = 1 + data = length 24, hash 3C664DB7 + sample 93: + time = 1860000 + flags = 1 + data = length 24, hash 9354B576 + sample 94: + time = 1880000 + flags = 1 + data = length 24, hash B5B9C14E + sample 95: + time = 1900000 + flags = 1 + data = length 24, hash 7DA9C98F + sample 96: + time = 1920000 + flags = 1 + data = length 24, hash EFEE54C6 + sample 97: + time = 1940000 + flags = 1 + data = length 24, hash 79DC8CBD + sample 98: + time = 1960000 + flags = 1 + data = length 24, hash A71A475C + sample 99: + time = 1980000 + flags = 1 + data = length 24, hash CA1CBB94 + sample 100: + time = 2000000 + flags = 1 + data = length 24, hash 91922226 + sample 101: + time = 2020000 + flags = 1 + data = length 24, hash C90278BC + sample 102: + time = 2040000 + flags = 1 + data = length 24, hash BD51986F + sample 103: + time = 2060000 + flags = 1 + data = length 24, hash 90AEF368 + sample 104: + time = 2080000 + flags = 1 + data = length 24, hash 1D83C955 + sample 105: + time = 2100000 + flags = 1 + data = length 24, hash 8FA9A915 + sample 106: + time = 2120000 + flags = 1 + data = length 24, hash C6C753E0 + sample 107: + time = 2140000 + flags = 1 + data = length 24, hash 85FA27A7 + sample 108: + time = 2160000 + flags = 1 + data = length 24, hash A0277324 + sample 109: + time = 2180000 + flags = 1 + data = length 24, hash B7696535 + sample 110: + time = 2200000 + flags = 1 + data = length 24, hash D69D668C + sample 111: + time = 2220000 + flags = 1 + data = length 24, hash 34C057CD + sample 112: + time = 2240000 + flags = 1 + data = length 24, hash 4EC5E974 + sample 113: + time = 2260000 + flags = 1 + data = length 24, hash 1C1CD40D + sample 114: + time = 2280000 + flags = 1 + data = length 24, hash 76CC54BC + sample 115: + time = 2300000 + flags = 1 + data = length 24, hash D497ACF5 + sample 116: + time = 2320000 + flags = 1 + data = length 24, hash A1386080 + sample 117: + time = 2340000 + flags = 1 + data = length 24, hash 7ED36954 + sample 118: + time = 2360000 + flags = 1 + data = length 24, hash C11A3BF9 + sample 119: + time = 2380000 + flags = 1 + data = length 24, hash 8FB69488 + sample 120: + time = 2400000 + flags = 1 + data = length 24, hash C6225F59 + sample 121: + time = 2420000 + flags = 1 + data = length 24, hash 122AB6D2 + sample 122: + time = 2440000 + flags = 1 + data = length 24, hash 1E195E7D + sample 123: + time = 2460000 + flags = 1 + data = length 24, hash BD3DF418 + sample 124: + time = 2480000 + flags = 1 + data = length 24, hash D8AE4A5 + sample 125: + time = 2500000 + flags = 1 + data = length 24, hash 977BD182 + sample 126: + time = 2520000 + flags = 1 + data = length 24, hash F361F060 + sample 127: + time = 2540000 + flags = 1 + data = length 24, hash 11EC8CD0 + sample 128: + time = 2560000 + flags = 1 + data = length 24, hash 3798F3D2 + sample 129: + time = 2580000 + flags = 1 + data = length 24, hash B2C2517C + sample 130: + time = 2600000 + flags = 1 + data = length 24, hash FBE0D0D8 + sample 131: + time = 2620000 + flags = 1 + data = length 24, hash 7033172F + sample 132: + time = 2640000 + flags = 1 + data = length 24, hash BE760029 + sample 133: + time = 2660000 + flags = 1 + data = length 24, hash 590AF28C + sample 134: + time = 2680000 + flags = 1 + data = length 24, hash AD28C48F + sample 135: + time = 2700000 + flags = 1 + data = length 24, hash 640AA61B + sample 136: + time = 2720000 + flags = 1 + data = length 24, hash ABE659B + sample 137: + time = 2740000 + flags = 1 + data = length 24, hash ED2691D2 + sample 138: + time = 2760000 + flags = 1 + data = length 24, hash D998C80E + sample 139: + time = 2780000 + flags = 1 + data = length 24, hash 8DC0DF5C + sample 140: + time = 2800000 + flags = 1 + data = length 24, hash 7692247B + sample 141: + time = 2820000 + flags = 1 + data = length 24, hash C1D1CCB9 + sample 142: + time = 2840000 + flags = 1 + data = length 24, hash 362CE78E + sample 143: + time = 2860000 + flags = 1 + data = length 24, hash 54FA84A + sample 144: + time = 2880000 + flags = 1 + data = length 24, hash 29E88C84 + sample 145: + time = 2900000 + flags = 1 + data = length 24, hash 1CD848AC + sample 146: + time = 2920000 + flags = 1 + data = length 24, hash 5C3D4A79 + sample 147: + time = 2940000 + flags = 1 + data = length 24, hash 1AA8E604 + sample 148: + time = 2960000 + flags = 1 + data = length 24, hash 186A4316 + sample 149: + time = 2980000 + flags = 1 + data = length 24, hash 61ACE481 + sample 150: + time = 3000000 + flags = 1 + data = length 24, hash D0C42780 + sample 151: + time = 3020000 + flags = 1 + data = length 24, hash FAD51BA1 + sample 152: + time = 3040000 + flags = 1 + data = length 24, hash F1A9AC71 + sample 153: + time = 3060000 + flags = 1 + data = length 24, hash 24425449 + sample 154: + time = 3080000 + flags = 1 + data = length 24, hash 37AAC3E6 + sample 155: + time = 3100000 + flags = 1 + data = length 24, hash 91F68CB4 + sample 156: + time = 3120000 + flags = 1 + data = length 24, hash F8C92820 + sample 157: + time = 3140000 + flags = 1 + data = length 24, hash ECD39C3E + sample 158: + time = 3160000 + flags = 1 + data = length 24, hash B27D8F78 + sample 159: + time = 3180000 + flags = 1 + data = length 24, hash C9EB3DFB + sample 160: + time = 3200000 + flags = 1 + data = length 24, hash 88DC54A2 + sample 161: + time = 3220000 + flags = 1 + data = length 24, hash 7FC4C5BE + sample 162: + time = 3240000 + flags = 1 + data = length 24, hash E4F684EF + sample 163: + time = 3260000 + flags = 1 + data = length 24, hash 55C08B56 + sample 164: + time = 3280000 + flags = 1 + data = length 24, hash E5A0F006 + sample 165: + time = 3300000 + flags = 1 + data = length 24, hash DE3F3AA7 + sample 166: + time = 3320000 + flags = 1 + data = length 24, hash 3F28AE7F + sample 167: + time = 3340000 + flags = 1 + data = length 24, hash 3949CAFF + sample 168: + time = 3360000 + flags = 1 + data = length 24, hash 772665A0 +tracksEnded = true diff --git a/library/core/src/test/assets/amr/sample_wb_cbr.amr.1.dump b/library/core/src/test/assets/amr/sample_wb_cbr.amr.1.dump new file mode 100644 index 0000000000..fad4565195 --- /dev/null +++ b/library/core/src/test/assets/amr/sample_wb_cbr.amr.1.dump @@ -0,0 +1,482 @@ +seekMap: + isSeekable = true + duration = 3380000 + getPosition(0) = [[timeUs=0, position=9]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = null + containerMimeType = null + sampleMimeType = audio/amr-wb + maxInputSize = 61 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 16000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 2712 + sample count = 113 + sample 0: + time = 1120000 + flags = 1 + data = length 24, hash EEB921BD + sample 1: + time = 1140000 + flags = 1 + data = length 24, hash 8B941A4C + sample 2: + time = 1160000 + flags = 1 + data = length 24, hash ED5F5FEE + sample 3: + time = 1180000 + flags = 1 + data = length 24, hash A588E0BB + sample 4: + time = 1200000 + flags = 1 + data = length 24, hash 588CBC01 + sample 5: + time = 1220000 + flags = 1 + data = length 24, hash DE22266C + sample 6: + time = 1240000 + flags = 1 + data = length 24, hash 921B6E5C + sample 7: + time = 1260000 + flags = 1 + data = length 24, hash EC11F041 + sample 8: + time = 1280000 + flags = 1 + data = length 24, hash 5BA9E0A3 + sample 9: + time = 1300000 + flags = 1 + data = length 24, hash DB6D52F3 + sample 10: + time = 1320000 + flags = 1 + data = length 24, hash 8EEBE525 + sample 11: + time = 1340000 + flags = 1 + data = length 24, hash 47A742AE + sample 12: + time = 1360000 + flags = 1 + data = length 24, hash E93F1E03 + sample 13: + time = 1380000 + flags = 1 + data = length 24, hash 3251F57C + sample 14: + time = 1400000 + flags = 1 + data = length 24, hash 3EDBBBDD + sample 15: + time = 1420000 + flags = 1 + data = length 24, hash 2E98465A + sample 16: + time = 1440000 + flags = 1 + data = length 24, hash A09EA52E + sample 17: + time = 1460000 + flags = 1 + data = length 24, hash A2A86FA6 + sample 18: + time = 1480000 + flags = 1 + data = length 24, hash 71DCD51C + sample 19: + time = 1500000 + flags = 1 + data = length 24, hash 2B02DEE1 + sample 20: + time = 1520000 + flags = 1 + data = length 24, hash 7A725192 + sample 21: + time = 1540000 + flags = 1 + data = length 24, hash 929AD483 + sample 22: + time = 1560000 + flags = 1 + data = length 24, hash 68440BF5 + sample 23: + time = 1580000 + flags = 1 + data = length 24, hash 5BD41AD6 + sample 24: + time = 1600000 + flags = 1 + data = length 24, hash 91A381 + sample 25: + time = 1620000 + flags = 1 + data = length 24, hash 8010C408 + sample 26: + time = 1640000 + flags = 1 + data = length 24, hash 482274BE + sample 27: + time = 1660000 + flags = 1 + data = length 24, hash D7DB8BCC + sample 28: + time = 1680000 + flags = 1 + data = length 24, hash 680BD9DD + sample 29: + time = 1700000 + flags = 1 + data = length 24, hash E313577C + sample 30: + time = 1720000 + flags = 1 + data = length 24, hash 9C10B0CD + sample 31: + time = 1740000 + flags = 1 + data = length 24, hash 2D90AC02 + sample 32: + time = 1760000 + flags = 1 + data = length 24, hash 64E8C245 + sample 33: + time = 1780000 + flags = 1 + data = length 24, hash 3954AC1B + sample 34: + time = 1800000 + flags = 1 + data = length 24, hash ACB8999F + sample 35: + time = 1820000 + flags = 1 + data = length 24, hash 43AE3957 + sample 36: + time = 1840000 + flags = 1 + data = length 24, hash 3C664DB7 + sample 37: + time = 1860000 + flags = 1 + data = length 24, hash 9354B576 + sample 38: + time = 1880000 + flags = 1 + data = length 24, hash B5B9C14E + sample 39: + time = 1900000 + flags = 1 + data = length 24, hash 7DA9C98F + sample 40: + time = 1920000 + flags = 1 + data = length 24, hash EFEE54C6 + sample 41: + time = 1940000 + flags = 1 + data = length 24, hash 79DC8CBD + sample 42: + time = 1960000 + flags = 1 + data = length 24, hash A71A475C + sample 43: + time = 1980000 + flags = 1 + data = length 24, hash CA1CBB94 + sample 44: + time = 2000000 + flags = 1 + data = length 24, hash 91922226 + sample 45: + time = 2020000 + flags = 1 + data = length 24, hash C90278BC + sample 46: + time = 2040000 + flags = 1 + data = length 24, hash BD51986F + sample 47: + time = 2060000 + flags = 1 + data = length 24, hash 90AEF368 + sample 48: + time = 2080000 + flags = 1 + data = length 24, hash 1D83C955 + sample 49: + time = 2100000 + flags = 1 + data = length 24, hash 8FA9A915 + sample 50: + time = 2120000 + flags = 1 + data = length 24, hash C6C753E0 + sample 51: + time = 2140000 + flags = 1 + data = length 24, hash 85FA27A7 + sample 52: + time = 2160000 + flags = 1 + data = length 24, hash A0277324 + sample 53: + time = 2180000 + flags = 1 + data = length 24, hash B7696535 + sample 54: + time = 2200000 + flags = 1 + data = length 24, hash D69D668C + sample 55: + time = 2220000 + flags = 1 + data = length 24, hash 34C057CD + sample 56: + time = 2240000 + flags = 1 + data = length 24, hash 4EC5E974 + sample 57: + time = 2260000 + flags = 1 + data = length 24, hash 1C1CD40D + sample 58: + time = 2280000 + flags = 1 + data = length 24, hash 76CC54BC + sample 59: + time = 2300000 + flags = 1 + data = length 24, hash D497ACF5 + sample 60: + time = 2320000 + flags = 1 + data = length 24, hash A1386080 + sample 61: + time = 2340000 + flags = 1 + data = length 24, hash 7ED36954 + sample 62: + time = 2360000 + flags = 1 + data = length 24, hash C11A3BF9 + sample 63: + time = 2380000 + flags = 1 + data = length 24, hash 8FB69488 + sample 64: + time = 2400000 + flags = 1 + data = length 24, hash C6225F59 + sample 65: + time = 2420000 + flags = 1 + data = length 24, hash 122AB6D2 + sample 66: + time = 2440000 + flags = 1 + data = length 24, hash 1E195E7D + sample 67: + time = 2460000 + flags = 1 + data = length 24, hash BD3DF418 + sample 68: + time = 2480000 + flags = 1 + data = length 24, hash D8AE4A5 + sample 69: + time = 2500000 + flags = 1 + data = length 24, hash 977BD182 + sample 70: + time = 2520000 + flags = 1 + data = length 24, hash F361F060 + sample 71: + time = 2540000 + flags = 1 + data = length 24, hash 11EC8CD0 + sample 72: + time = 2560000 + flags = 1 + data = length 24, hash 3798F3D2 + sample 73: + time = 2580000 + flags = 1 + data = length 24, hash B2C2517C + sample 74: + time = 2600000 + flags = 1 + data = length 24, hash FBE0D0D8 + sample 75: + time = 2620000 + flags = 1 + data = length 24, hash 7033172F + sample 76: + time = 2640000 + flags = 1 + data = length 24, hash BE760029 + sample 77: + time = 2660000 + flags = 1 + data = length 24, hash 590AF28C + sample 78: + time = 2680000 + flags = 1 + data = length 24, hash AD28C48F + sample 79: + time = 2700000 + flags = 1 + data = length 24, hash 640AA61B + sample 80: + time = 2720000 + flags = 1 + data = length 24, hash ABE659B + sample 81: + time = 2740000 + flags = 1 + data = length 24, hash ED2691D2 + sample 82: + time = 2760000 + flags = 1 + data = length 24, hash D998C80E + sample 83: + time = 2780000 + flags = 1 + data = length 24, hash 8DC0DF5C + sample 84: + time = 2800000 + flags = 1 + data = length 24, hash 7692247B + sample 85: + time = 2820000 + flags = 1 + data = length 24, hash C1D1CCB9 + sample 86: + time = 2840000 + flags = 1 + data = length 24, hash 362CE78E + sample 87: + time = 2860000 + flags = 1 + data = length 24, hash 54FA84A + sample 88: + time = 2880000 + flags = 1 + data = length 24, hash 29E88C84 + sample 89: + time = 2900000 + flags = 1 + data = length 24, hash 1CD848AC + sample 90: + time = 2920000 + flags = 1 + data = length 24, hash 5C3D4A79 + sample 91: + time = 2940000 + flags = 1 + data = length 24, hash 1AA8E604 + sample 92: + time = 2960000 + flags = 1 + data = length 24, hash 186A4316 + sample 93: + time = 2980000 + flags = 1 + data = length 24, hash 61ACE481 + sample 94: + time = 3000000 + flags = 1 + data = length 24, hash D0C42780 + sample 95: + time = 3020000 + flags = 1 + data = length 24, hash FAD51BA1 + sample 96: + time = 3040000 + flags = 1 + data = length 24, hash F1A9AC71 + sample 97: + time = 3060000 + flags = 1 + data = length 24, hash 24425449 + sample 98: + time = 3080000 + flags = 1 + data = length 24, hash 37AAC3E6 + sample 99: + time = 3100000 + flags = 1 + data = length 24, hash 91F68CB4 + sample 100: + time = 3120000 + flags = 1 + data = length 24, hash F8C92820 + sample 101: + time = 3140000 + flags = 1 + data = length 24, hash ECD39C3E + sample 102: + time = 3160000 + flags = 1 + data = length 24, hash B27D8F78 + sample 103: + time = 3180000 + flags = 1 + data = length 24, hash C9EB3DFB + sample 104: + time = 3200000 + flags = 1 + data = length 24, hash 88DC54A2 + sample 105: + time = 3220000 + flags = 1 + data = length 24, hash 7FC4C5BE + sample 106: + time = 3240000 + flags = 1 + data = length 24, hash E4F684EF + sample 107: + time = 3260000 + flags = 1 + data = length 24, hash 55C08B56 + sample 108: + time = 3280000 + flags = 1 + data = length 24, hash E5A0F006 + sample 109: + time = 3300000 + flags = 1 + data = length 24, hash DE3F3AA7 + sample 110: + time = 3320000 + flags = 1 + data = length 24, hash 3F28AE7F + sample 111: + time = 3340000 + flags = 1 + data = length 24, hash 3949CAFF + sample 112: + time = 3360000 + flags = 1 + data = length 24, hash 772665A0 +tracksEnded = true diff --git a/library/core/src/test/assets/amr/sample_wb_cbr.amr.2.dump b/library/core/src/test/assets/amr/sample_wb_cbr.amr.2.dump new file mode 100644 index 0000000000..1f00a90739 --- /dev/null +++ b/library/core/src/test/assets/amr/sample_wb_cbr.amr.2.dump @@ -0,0 +1,258 @@ +seekMap: + isSeekable = true + duration = 3380000 + getPosition(0) = [[timeUs=0, position=9]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = null + containerMimeType = null + sampleMimeType = audio/amr-wb + maxInputSize = 61 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 16000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 1368 + sample count = 57 + sample 0: + time = 2240000 + flags = 1 + data = length 24, hash 4EC5E974 + sample 1: + time = 2260000 + flags = 1 + data = length 24, hash 1C1CD40D + sample 2: + time = 2280000 + flags = 1 + data = length 24, hash 76CC54BC + sample 3: + time = 2300000 + flags = 1 + data = length 24, hash D497ACF5 + sample 4: + time = 2320000 + flags = 1 + data = length 24, hash A1386080 + sample 5: + time = 2340000 + flags = 1 + data = length 24, hash 7ED36954 + sample 6: + time = 2360000 + flags = 1 + data = length 24, hash C11A3BF9 + sample 7: + time = 2380000 + flags = 1 + data = length 24, hash 8FB69488 + sample 8: + time = 2400000 + flags = 1 + data = length 24, hash C6225F59 + sample 9: + time = 2420000 + flags = 1 + data = length 24, hash 122AB6D2 + sample 10: + time = 2440000 + flags = 1 + data = length 24, hash 1E195E7D + sample 11: + time = 2460000 + flags = 1 + data = length 24, hash BD3DF418 + sample 12: + time = 2480000 + flags = 1 + data = length 24, hash D8AE4A5 + sample 13: + time = 2500000 + flags = 1 + data = length 24, hash 977BD182 + sample 14: + time = 2520000 + flags = 1 + data = length 24, hash F361F060 + sample 15: + time = 2540000 + flags = 1 + data = length 24, hash 11EC8CD0 + sample 16: + time = 2560000 + flags = 1 + data = length 24, hash 3798F3D2 + sample 17: + time = 2580000 + flags = 1 + data = length 24, hash B2C2517C + sample 18: + time = 2600000 + flags = 1 + data = length 24, hash FBE0D0D8 + sample 19: + time = 2620000 + flags = 1 + data = length 24, hash 7033172F + sample 20: + time = 2640000 + flags = 1 + data = length 24, hash BE760029 + sample 21: + time = 2660000 + flags = 1 + data = length 24, hash 590AF28C + sample 22: + time = 2680000 + flags = 1 + data = length 24, hash AD28C48F + sample 23: + time = 2700000 + flags = 1 + data = length 24, hash 640AA61B + sample 24: + time = 2720000 + flags = 1 + data = length 24, hash ABE659B + sample 25: + time = 2740000 + flags = 1 + data = length 24, hash ED2691D2 + sample 26: + time = 2760000 + flags = 1 + data = length 24, hash D998C80E + sample 27: + time = 2780000 + flags = 1 + data = length 24, hash 8DC0DF5C + sample 28: + time = 2800000 + flags = 1 + data = length 24, hash 7692247B + sample 29: + time = 2820000 + flags = 1 + data = length 24, hash C1D1CCB9 + sample 30: + time = 2840000 + flags = 1 + data = length 24, hash 362CE78E + sample 31: + time = 2860000 + flags = 1 + data = length 24, hash 54FA84A + sample 32: + time = 2880000 + flags = 1 + data = length 24, hash 29E88C84 + sample 33: + time = 2900000 + flags = 1 + data = length 24, hash 1CD848AC + sample 34: + time = 2920000 + flags = 1 + data = length 24, hash 5C3D4A79 + sample 35: + time = 2940000 + flags = 1 + data = length 24, hash 1AA8E604 + sample 36: + time = 2960000 + flags = 1 + data = length 24, hash 186A4316 + sample 37: + time = 2980000 + flags = 1 + data = length 24, hash 61ACE481 + sample 38: + time = 3000000 + flags = 1 + data = length 24, hash D0C42780 + sample 39: + time = 3020000 + flags = 1 + data = length 24, hash FAD51BA1 + sample 40: + time = 3040000 + flags = 1 + data = length 24, hash F1A9AC71 + sample 41: + time = 3060000 + flags = 1 + data = length 24, hash 24425449 + sample 42: + time = 3080000 + flags = 1 + data = length 24, hash 37AAC3E6 + sample 43: + time = 3100000 + flags = 1 + data = length 24, hash 91F68CB4 + sample 44: + time = 3120000 + flags = 1 + data = length 24, hash F8C92820 + sample 45: + time = 3140000 + flags = 1 + data = length 24, hash ECD39C3E + sample 46: + time = 3160000 + flags = 1 + data = length 24, hash B27D8F78 + sample 47: + time = 3180000 + flags = 1 + data = length 24, hash C9EB3DFB + sample 48: + time = 3200000 + flags = 1 + data = length 24, hash 88DC54A2 + sample 49: + time = 3220000 + flags = 1 + data = length 24, hash 7FC4C5BE + sample 50: + time = 3240000 + flags = 1 + data = length 24, hash E4F684EF + sample 51: + time = 3260000 + flags = 1 + data = length 24, hash 55C08B56 + sample 52: + time = 3280000 + flags = 1 + data = length 24, hash E5A0F006 + sample 53: + time = 3300000 + flags = 1 + data = length 24, hash DE3F3AA7 + sample 54: + time = 3320000 + flags = 1 + data = length 24, hash 3F28AE7F + sample 55: + time = 3340000 + flags = 1 + data = length 24, hash 3949CAFF + sample 56: + time = 3360000 + flags = 1 + data = length 24, hash 772665A0 +tracksEnded = true diff --git a/library/core/src/test/assets/amr/sample_wb_cbr.amr.3.dump b/library/core/src/test/assets/amr/sample_wb_cbr.amr.3.dump new file mode 100644 index 0000000000..1ec8c6fdb7 --- /dev/null +++ b/library/core/src/test/assets/amr/sample_wb_cbr.amr.3.dump @@ -0,0 +1,34 @@ +seekMap: + isSeekable = true + duration = 3380000 + getPosition(0) = [[timeUs=0, position=9]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = null + containerMimeType = null + sampleMimeType = audio/amr-wb + maxInputSize = 61 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 16000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 24 + sample count = 1 + sample 0: + time = 3360000 + flags = 1 + data = length 24, hash 772665A0 +tracksEnded = true diff --git a/library/core/src/test/assets/amr/sample_wb_cbr.amr.unklen.dump b/library/core/src/test/assets/amr/sample_wb_cbr.amr.unklen.dump new file mode 100644 index 0000000000..1b3b8bd0dd --- /dev/null +++ b/library/core/src/test/assets/amr/sample_wb_cbr.amr.unklen.dump @@ -0,0 +1,706 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = null + containerMimeType = null + sampleMimeType = audio/amr-wb + maxInputSize = 61 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 16000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 4056 + sample count = 169 + sample 0: + time = 0 + flags = 1 + data = length 24, hash C3025798 + sample 1: + time = 20000 + flags = 1 + data = length 24, hash 39CABAE9 + sample 2: + time = 40000 + flags = 1 + data = length 24, hash 2752F470 + sample 3: + time = 60000 + flags = 1 + data = length 24, hash 394F76F6 + sample 4: + time = 80000 + flags = 1 + data = length 24, hash FF9EEF + sample 5: + time = 100000 + flags = 1 + data = length 24, hash 54ECB1B4 + sample 6: + time = 120000 + flags = 1 + data = length 24, hash 6D7A3A5F + sample 7: + time = 140000 + flags = 1 + data = length 24, hash 684CD144 + sample 8: + time = 160000 + flags = 1 + data = length 24, hash 87B7D176 + sample 9: + time = 180000 + flags = 1 + data = length 24, hash 4C02F9A5 + sample 10: + time = 200000 + flags = 1 + data = length 24, hash B4154108 + sample 11: + time = 220000 + flags = 1 + data = length 24, hash 4448F477 + sample 12: + time = 240000 + flags = 1 + data = length 24, hash 755A4939 + sample 13: + time = 260000 + flags = 1 + data = length 24, hash 8C8BC6C3 + sample 14: + time = 280000 + flags = 1 + data = length 24, hash BC37F63F + sample 15: + time = 300000 + flags = 1 + data = length 24, hash 3352C43C + sample 16: + time = 320000 + flags = 1 + data = length 24, hash 7998E1F2 + sample 17: + time = 340000 + flags = 1 + data = length 24, hash A8ECBEFC + sample 18: + time = 360000 + flags = 1 + data = length 24, hash 944AC118 + sample 19: + time = 380000 + flags = 1 + data = length 24, hash FD2C8E1F + sample 20: + time = 400000 + flags = 1 + data = length 24, hash B3D867AF + sample 21: + time = 420000 + flags = 1 + data = length 24, hash 3DC6E592 + sample 22: + time = 440000 + flags = 1 + data = length 24, hash 32B276CD + sample 23: + time = 460000 + flags = 1 + data = length 24, hash 5488AEF3 + sample 24: + time = 480000 + flags = 1 + data = length 24, hash 7A4D516 + sample 25: + time = 500000 + flags = 1 + data = length 24, hash 570AE83F + sample 26: + time = 520000 + flags = 1 + data = length 24, hash E5CB3477 + sample 27: + time = 540000 + flags = 1 + data = length 24, hash E04C00E4 + sample 28: + time = 560000 + flags = 1 + data = length 24, hash 21B7C97 + sample 29: + time = 580000 + flags = 1 + data = length 24, hash 1633F470 + sample 30: + time = 600000 + flags = 1 + data = length 24, hash 28D65CA6 + sample 31: + time = 620000 + flags = 1 + data = length 24, hash CC6A675C + sample 32: + time = 640000 + flags = 1 + data = length 24, hash 4C91080A + sample 33: + time = 660000 + flags = 1 + data = length 24, hash F6482FB5 + sample 34: + time = 680000 + flags = 1 + data = length 24, hash 2C76F48C + sample 35: + time = 700000 + flags = 1 + data = length 24, hash 6E3B0D72 + sample 36: + time = 720000 + flags = 1 + data = length 24, hash 799AA003 + sample 37: + time = 740000 + flags = 1 + data = length 24, hash DFC0BA81 + sample 38: + time = 760000 + flags = 1 + data = length 24, hash CBDF3826 + sample 39: + time = 780000 + flags = 1 + data = length 24, hash 16862B75 + sample 40: + time = 800000 + flags = 1 + data = length 24, hash 865A828E + sample 41: + time = 820000 + flags = 1 + data = length 24, hash 336BBDC9 + sample 42: + time = 840000 + flags = 1 + data = length 24, hash 6CFC6C34 + sample 43: + time = 860000 + flags = 1 + data = length 24, hash 32C8CD46 + sample 44: + time = 880000 + flags = 1 + data = length 24, hash 9FE11C4C + sample 45: + time = 900000 + flags = 1 + data = length 24, hash AA5A12B7 + sample 46: + time = 920000 + flags = 1 + data = length 24, hash AA0F4A4D + sample 47: + time = 940000 + flags = 1 + data = length 24, hash 34415484 + sample 48: + time = 960000 + flags = 1 + data = length 24, hash 5018928E + sample 49: + time = 980000 + flags = 1 + data = length 24, hash 4A04D162 + sample 50: + time = 1000000 + flags = 1 + data = length 24, hash 4C70F9F0 + sample 51: + time = 1020000 + flags = 1 + data = length 24, hash 99EF3168 + sample 52: + time = 1040000 + flags = 1 + data = length 24, hash C600DAF + sample 53: + time = 1060000 + flags = 1 + data = length 24, hash FDBB192E + sample 54: + time = 1080000 + flags = 1 + data = length 24, hash 99096A48 + sample 55: + time = 1100000 + flags = 1 + data = length 24, hash D793F88B + sample 56: + time = 1120000 + flags = 1 + data = length 24, hash EEB921BD + sample 57: + time = 1140000 + flags = 1 + data = length 24, hash 8B941A4C + sample 58: + time = 1160000 + flags = 1 + data = length 24, hash ED5F5FEE + sample 59: + time = 1180000 + flags = 1 + data = length 24, hash A588E0BB + sample 60: + time = 1200000 + flags = 1 + data = length 24, hash 588CBC01 + sample 61: + time = 1220000 + flags = 1 + data = length 24, hash DE22266C + sample 62: + time = 1240000 + flags = 1 + data = length 24, hash 921B6E5C + sample 63: + time = 1260000 + flags = 1 + data = length 24, hash EC11F041 + sample 64: + time = 1280000 + flags = 1 + data = length 24, hash 5BA9E0A3 + sample 65: + time = 1300000 + flags = 1 + data = length 24, hash DB6D52F3 + sample 66: + time = 1320000 + flags = 1 + data = length 24, hash 8EEBE525 + sample 67: + time = 1340000 + flags = 1 + data = length 24, hash 47A742AE + sample 68: + time = 1360000 + flags = 1 + data = length 24, hash E93F1E03 + sample 69: + time = 1380000 + flags = 1 + data = length 24, hash 3251F57C + sample 70: + time = 1400000 + flags = 1 + data = length 24, hash 3EDBBBDD + sample 71: + time = 1420000 + flags = 1 + data = length 24, hash 2E98465A + sample 72: + time = 1440000 + flags = 1 + data = length 24, hash A09EA52E + sample 73: + time = 1460000 + flags = 1 + data = length 24, hash A2A86FA6 + sample 74: + time = 1480000 + flags = 1 + data = length 24, hash 71DCD51C + sample 75: + time = 1500000 + flags = 1 + data = length 24, hash 2B02DEE1 + sample 76: + time = 1520000 + flags = 1 + data = length 24, hash 7A725192 + sample 77: + time = 1540000 + flags = 1 + data = length 24, hash 929AD483 + sample 78: + time = 1560000 + flags = 1 + data = length 24, hash 68440BF5 + sample 79: + time = 1580000 + flags = 1 + data = length 24, hash 5BD41AD6 + sample 80: + time = 1600000 + flags = 1 + data = length 24, hash 91A381 + sample 81: + time = 1620000 + flags = 1 + data = length 24, hash 8010C408 + sample 82: + time = 1640000 + flags = 1 + data = length 24, hash 482274BE + sample 83: + time = 1660000 + flags = 1 + data = length 24, hash D7DB8BCC + sample 84: + time = 1680000 + flags = 1 + data = length 24, hash 680BD9DD + sample 85: + time = 1700000 + flags = 1 + data = length 24, hash E313577C + sample 86: + time = 1720000 + flags = 1 + data = length 24, hash 9C10B0CD + sample 87: + time = 1740000 + flags = 1 + data = length 24, hash 2D90AC02 + sample 88: + time = 1760000 + flags = 1 + data = length 24, hash 64E8C245 + sample 89: + time = 1780000 + flags = 1 + data = length 24, hash 3954AC1B + sample 90: + time = 1800000 + flags = 1 + data = length 24, hash ACB8999F + sample 91: + time = 1820000 + flags = 1 + data = length 24, hash 43AE3957 + sample 92: + time = 1840000 + flags = 1 + data = length 24, hash 3C664DB7 + sample 93: + time = 1860000 + flags = 1 + data = length 24, hash 9354B576 + sample 94: + time = 1880000 + flags = 1 + data = length 24, hash B5B9C14E + sample 95: + time = 1900000 + flags = 1 + data = length 24, hash 7DA9C98F + sample 96: + time = 1920000 + flags = 1 + data = length 24, hash EFEE54C6 + sample 97: + time = 1940000 + flags = 1 + data = length 24, hash 79DC8CBD + sample 98: + time = 1960000 + flags = 1 + data = length 24, hash A71A475C + sample 99: + time = 1980000 + flags = 1 + data = length 24, hash CA1CBB94 + sample 100: + time = 2000000 + flags = 1 + data = length 24, hash 91922226 + sample 101: + time = 2020000 + flags = 1 + data = length 24, hash C90278BC + sample 102: + time = 2040000 + flags = 1 + data = length 24, hash BD51986F + sample 103: + time = 2060000 + flags = 1 + data = length 24, hash 90AEF368 + sample 104: + time = 2080000 + flags = 1 + data = length 24, hash 1D83C955 + sample 105: + time = 2100000 + flags = 1 + data = length 24, hash 8FA9A915 + sample 106: + time = 2120000 + flags = 1 + data = length 24, hash C6C753E0 + sample 107: + time = 2140000 + flags = 1 + data = length 24, hash 85FA27A7 + sample 108: + time = 2160000 + flags = 1 + data = length 24, hash A0277324 + sample 109: + time = 2180000 + flags = 1 + data = length 24, hash B7696535 + sample 110: + time = 2200000 + flags = 1 + data = length 24, hash D69D668C + sample 111: + time = 2220000 + flags = 1 + data = length 24, hash 34C057CD + sample 112: + time = 2240000 + flags = 1 + data = length 24, hash 4EC5E974 + sample 113: + time = 2260000 + flags = 1 + data = length 24, hash 1C1CD40D + sample 114: + time = 2280000 + flags = 1 + data = length 24, hash 76CC54BC + sample 115: + time = 2300000 + flags = 1 + data = length 24, hash D497ACF5 + sample 116: + time = 2320000 + flags = 1 + data = length 24, hash A1386080 + sample 117: + time = 2340000 + flags = 1 + data = length 24, hash 7ED36954 + sample 118: + time = 2360000 + flags = 1 + data = length 24, hash C11A3BF9 + sample 119: + time = 2380000 + flags = 1 + data = length 24, hash 8FB69488 + sample 120: + time = 2400000 + flags = 1 + data = length 24, hash C6225F59 + sample 121: + time = 2420000 + flags = 1 + data = length 24, hash 122AB6D2 + sample 122: + time = 2440000 + flags = 1 + data = length 24, hash 1E195E7D + sample 123: + time = 2460000 + flags = 1 + data = length 24, hash BD3DF418 + sample 124: + time = 2480000 + flags = 1 + data = length 24, hash D8AE4A5 + sample 125: + time = 2500000 + flags = 1 + data = length 24, hash 977BD182 + sample 126: + time = 2520000 + flags = 1 + data = length 24, hash F361F060 + sample 127: + time = 2540000 + flags = 1 + data = length 24, hash 11EC8CD0 + sample 128: + time = 2560000 + flags = 1 + data = length 24, hash 3798F3D2 + sample 129: + time = 2580000 + flags = 1 + data = length 24, hash B2C2517C + sample 130: + time = 2600000 + flags = 1 + data = length 24, hash FBE0D0D8 + sample 131: + time = 2620000 + flags = 1 + data = length 24, hash 7033172F + sample 132: + time = 2640000 + flags = 1 + data = length 24, hash BE760029 + sample 133: + time = 2660000 + flags = 1 + data = length 24, hash 590AF28C + sample 134: + time = 2680000 + flags = 1 + data = length 24, hash AD28C48F + sample 135: + time = 2700000 + flags = 1 + data = length 24, hash 640AA61B + sample 136: + time = 2720000 + flags = 1 + data = length 24, hash ABE659B + sample 137: + time = 2740000 + flags = 1 + data = length 24, hash ED2691D2 + sample 138: + time = 2760000 + flags = 1 + data = length 24, hash D998C80E + sample 139: + time = 2780000 + flags = 1 + data = length 24, hash 8DC0DF5C + sample 140: + time = 2800000 + flags = 1 + data = length 24, hash 7692247B + sample 141: + time = 2820000 + flags = 1 + data = length 24, hash C1D1CCB9 + sample 142: + time = 2840000 + flags = 1 + data = length 24, hash 362CE78E + sample 143: + time = 2860000 + flags = 1 + data = length 24, hash 54FA84A + sample 144: + time = 2880000 + flags = 1 + data = length 24, hash 29E88C84 + sample 145: + time = 2900000 + flags = 1 + data = length 24, hash 1CD848AC + sample 146: + time = 2920000 + flags = 1 + data = length 24, hash 5C3D4A79 + sample 147: + time = 2940000 + flags = 1 + data = length 24, hash 1AA8E604 + sample 148: + time = 2960000 + flags = 1 + data = length 24, hash 186A4316 + sample 149: + time = 2980000 + flags = 1 + data = length 24, hash 61ACE481 + sample 150: + time = 3000000 + flags = 1 + data = length 24, hash D0C42780 + sample 151: + time = 3020000 + flags = 1 + data = length 24, hash FAD51BA1 + sample 152: + time = 3040000 + flags = 1 + data = length 24, hash F1A9AC71 + sample 153: + time = 3060000 + flags = 1 + data = length 24, hash 24425449 + sample 154: + time = 3080000 + flags = 1 + data = length 24, hash 37AAC3E6 + sample 155: + time = 3100000 + flags = 1 + data = length 24, hash 91F68CB4 + sample 156: + time = 3120000 + flags = 1 + data = length 24, hash F8C92820 + sample 157: + time = 3140000 + flags = 1 + data = length 24, hash ECD39C3E + sample 158: + time = 3160000 + flags = 1 + data = length 24, hash B27D8F78 + sample 159: + time = 3180000 + flags = 1 + data = length 24, hash C9EB3DFB + sample 160: + time = 3200000 + flags = 1 + data = length 24, hash 88DC54A2 + sample 161: + time = 3220000 + flags = 1 + data = length 24, hash 7FC4C5BE + sample 162: + time = 3240000 + flags = 1 + data = length 24, hash E4F684EF + sample 163: + time = 3260000 + flags = 1 + data = length 24, hash 55C08B56 + sample 164: + time = 3280000 + flags = 1 + data = length 24, hash E5A0F006 + sample 165: + time = 3300000 + flags = 1 + data = length 24, hash DE3F3AA7 + sample 166: + time = 3320000 + flags = 1 + data = length 24, hash 3F28AE7F + sample 167: + time = 3340000 + flags = 1 + data = length 24, hash 3949CAFF + sample 168: + time = 3360000 + flags = 1 + data = length 24, hash 772665A0 +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/testvid_1022ms.mp4 b/library/core/src/test/assets/mp4/testvid_1022ms.mp4 new file mode 100644 index 0000000000..bbd2729c4d Binary files /dev/null and b/library/core/src/test/assets/mp4/testvid_1022ms.mp4 differ diff --git a/library/core/src/test/assets/ts/bbb_2500ms.ts b/library/core/src/test/assets/ts/bbb_2500ms.ts new file mode 100644 index 0000000000..34ab2e9bc3 Binary files /dev/null and b/library/core/src/test/assets/ts/bbb_2500ms.ts differ diff --git a/library/core/src/test/assets/ts/elephants_dream.mpg b/library/core/src/test/assets/ts/elephants_dream.mpg new file mode 100644 index 0000000000..05a1d17f4b Binary files /dev/null and b/library/core/src/test/assets/ts/elephants_dream.mpg differ diff --git a/library/core/src/test/assets/ts/sample.ps.0.dump b/library/core/src/test/assets/ts/sample.ps.0.dump index dda6de8ab4..06ef48de7a 100644 --- a/library/core/src/test/assets/ts/sample.ps.0.dump +++ b/library/core/src/test/assets/ts/sample.ps.0.dump @@ -1,6 +1,6 @@ seekMap: - isSeekable = false - duration = UNSET TIME + isSeekable = true + duration = 766 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 2 track 192: diff --git a/library/core/src/test/assets/ts/sample.ps.1.dump b/library/core/src/test/assets/ts/sample.ps.1.dump new file mode 100644 index 0000000000..ce0f223bd4 --- /dev/null +++ b/library/core/src/test/assets/ts/sample.ps.1.dump @@ -0,0 +1,59 @@ +seekMap: + isSeekable = true + duration = 766 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 192: + format: + bitrate = -1 + id = 192 + containerMimeType = null + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 0 + sample count = 0 +track 224: + format: + bitrate = -1 + id = 224 + containerMimeType = null + sampleMimeType = video/mpeg2 + maxInputSize = -1 + width = 640 + height = 426 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 22, hash 743CC6F8 + total output bytes = 33949 + sample count = 1 + sample 0: + time = 80000 + flags = 0 + data = length 17831, hash 5C5A57F5 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample.ps.2.dump b/library/core/src/test/assets/ts/sample.ps.2.dump new file mode 100644 index 0000000000..7d0a77037d --- /dev/null +++ b/library/core/src/test/assets/ts/sample.ps.2.dump @@ -0,0 +1,55 @@ +seekMap: + isSeekable = true + duration = 766 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 192: + format: + bitrate = -1 + id = 192 + containerMimeType = null + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 0 + sample count = 0 +track 224: + format: + bitrate = -1 + id = 224 + containerMimeType = null + sampleMimeType = video/mpeg2 + maxInputSize = -1 + width = 640 + height = 426 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 22, hash 743CC6F8 + total output bytes = 19791 + sample count = 0 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample.ps.3.dump b/library/core/src/test/assets/ts/sample.ps.3.dump new file mode 100644 index 0000000000..a7258cd7ef --- /dev/null +++ b/library/core/src/test/assets/ts/sample.ps.3.dump @@ -0,0 +1,55 @@ +seekMap: + isSeekable = true + duration = 766 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 192: + format: + bitrate = -1 + id = 192 + containerMimeType = null + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 0 + sample count = 0 +track 224: + format: + bitrate = -1 + id = 224 + containerMimeType = null + sampleMimeType = video/mpeg2 + maxInputSize = -1 + width = 640 + height = 426 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 22, hash 743CC6F8 + total output bytes = 1585 + sample count = 0 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample.ps.unklen.dump b/library/core/src/test/assets/ts/sample.ps.unklen.dump new file mode 100644 index 0000000000..dda6de8ab4 --- /dev/null +++ b/library/core/src/test/assets/ts/sample.ps.unklen.dump @@ -0,0 +1,79 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 192: + format: + bitrate = -1 + id = 192 + containerMimeType = null + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 1671 + sample count = 4 + sample 0: + time = 29088 + flags = 1 + data = length 417, hash 5C710F78 + sample 1: + time = 55210 + flags = 1 + data = length 418, hash 79CF71F8 + sample 2: + time = 81332 + flags = 1 + data = length 418, hash 79CF71F8 + sample 3: + time = 107454 + flags = 1 + data = length 418, hash 79CF71F8 +track 224: + format: + bitrate = -1 + id = 224 + containerMimeType = null + sampleMimeType = video/mpeg2 + maxInputSize = -1 + width = 640 + height = 426 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 22, hash 743CC6F8 + total output bytes = 44056 + sample count = 2 + sample 0: + time = 40000 + flags = 1 + data = length 20646, hash 576390B + sample 1: + time = 80000 + flags = 0 + data = length 17831, hash 5C5A57F5 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample.ts.0.dump b/library/core/src/test/assets/ts/sample.ts.0.dump index a74268a702..b45a32fd3a 100644 --- a/library/core/src/test/assets/ts/sample.ts.0.dump +++ b/library/core/src/test/assets/ts/sample.ts.0.dump @@ -1,8 +1,8 @@ seekMap: - isSeekable = false - duration = UNSET TIME + isSeekable = true + duration = 66733 getPosition(0) = [[timeUs=0, position=0]] -numberOfTracks = 2 +numberOfTracks = 3 track 256: format: bitrate = -1 @@ -76,4 +76,28 @@ track 257: time = 100822 flags = 1 data = length 1254, hash 73FB07B8 +track 8448: + format: + bitrate = -1 + id = 1/8448 + containerMimeType = null + sampleMimeType = application/cea-608 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 0 + sample count = 0 tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample.ts.1.dump b/library/core/src/test/assets/ts/sample.ts.1.dump new file mode 100644 index 0000000000..7454a02141 --- /dev/null +++ b/library/core/src/test/assets/ts/sample.ts.1.dump @@ -0,0 +1,99 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 3 +track 256: + format: + bitrate = -1 + id = 1/256 + containerMimeType = null + sampleMimeType = video/mpeg2 + maxInputSize = -1 + width = 640 + height = 426 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 22, hash CE183139 + total output bytes = 24315 + sample count = 1 + sample 0: + time = 55611 + flags = 0 + data = length 18112, hash EC44B35B +track 257: + format: + bitrate = -1 + id = 1/257 + containerMimeType = null + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + initializationData: + total output bytes = 5015 + sample count = 4 + sample 0: + time = 11333 + flags = 1 + data = length 1253, hash 727FD1C6 + sample 1: + time = 37455 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 2: + time = 63578 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 3: + time = 89700 + flags = 1 + data = length 1254, hash 73FB07B8 +track 8448: + format: + bitrate = -1 + id = 1/8448 + containerMimeType = null + sampleMimeType = application/cea-608 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 0 + sample count = 0 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample.ts.2.dump b/library/core/src/test/assets/ts/sample.ts.2.dump new file mode 100644 index 0000000000..c7cef05b93 --- /dev/null +++ b/library/core/src/test/assets/ts/sample.ts.2.dump @@ -0,0 +1,99 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 3 +track 256: + format: + bitrate = -1 + id = 1/256 + containerMimeType = null + sampleMimeType = video/mpeg2 + maxInputSize = -1 + width = 640 + height = 426 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 22, hash CE183139 + total output bytes = 24315 + sample count = 1 + sample 0: + time = 77855 + flags = 0 + data = length 18112, hash EC44B35B +track 257: + format: + bitrate = -1 + id = 1/257 + containerMimeType = null + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + initializationData: + total output bytes = 5015 + sample count = 4 + sample 0: + time = 33577 + flags = 1 + data = length 1253, hash 727FD1C6 + sample 1: + time = 59699 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 2: + time = 85822 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 3: + time = 111944 + flags = 1 + data = length 1254, hash 73FB07B8 +track 8448: + format: + bitrate = -1 + id = 1/8448 + containerMimeType = null + sampleMimeType = application/cea-608 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 0 + sample count = 0 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample.ts.3.dump b/library/core/src/test/assets/ts/sample.ts.3.dump new file mode 100644 index 0000000000..d8238e1626 --- /dev/null +++ b/library/core/src/test/assets/ts/sample.ts.3.dump @@ -0,0 +1,87 @@ +seekMap: + isSeekable = true + duration = 66733 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 3 +track 256: + format: + bitrate = -1 + id = 1/256 + containerMimeType = null + sampleMimeType = video/mpeg2 + maxInputSize = -1 + width = 640 + height = 426 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 22, hash CE183139 + total output bytes = 0 + sample count = 0 +track 257: + format: + bitrate = -1 + id = 1/257 + containerMimeType = null + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + initializationData: + total output bytes = 2508 + sample count = 2 + sample 0: + time = 66733 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 1: + time = 92855 + flags = 1 + data = length 1254, hash 73FB07B8 +track 8448: + format: + bitrate = -1 + id = 1/8448 + containerMimeType = null + sampleMimeType = application/cea-608 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 0 + sample count = 0 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample.ts.unklen.dump b/library/core/src/test/assets/ts/sample.ts.unklen.dump new file mode 100644 index 0000000000..56f6b01a9c --- /dev/null +++ b/library/core/src/test/assets/ts/sample.ts.unklen.dump @@ -0,0 +1,103 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 3 +track 256: + format: + bitrate = -1 + id = 1/256 + containerMimeType = null + sampleMimeType = video/mpeg2 + maxInputSize = -1 + width = 640 + height = 426 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 22, hash CE183139 + total output bytes = 45026 + sample count = 2 + sample 0: + time = 33366 + flags = 1 + data = length 20711, hash 34341E8 + sample 1: + time = 66733 + flags = 0 + data = length 18112, hash EC44B35B +track 257: + format: + bitrate = -1 + id = 1/257 + containerMimeType = null + sampleMimeType = audio/mpeg-L2 + maxInputSize = 4096 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + initializationData: + total output bytes = 5015 + sample count = 4 + sample 0: + time = 22455 + flags = 1 + data = length 1253, hash 727FD1C6 + sample 1: + time = 48577 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 2: + time = 74700 + flags = 1 + data = length 1254, hash 73FB07B8 + sample 3: + time = 100822 + flags = 1 + data = length 1254, hash 73FB07B8 +track 8448: + format: + bitrate = -1 + id = 1/8448 + containerMimeType = null + sampleMimeType = application/cea-608 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 0 + sample count = 0 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample_cbs.adts b/library/core/src/test/assets/ts/sample_cbs.adts new file mode 100644 index 0000000000..abbaad0daf Binary files /dev/null and b/library/core/src/test/assets/ts/sample_cbs.adts differ diff --git a/library/core/src/test/assets/ts/sample_cbs.adts.0.dump b/library/core/src/test/assets/ts/sample_cbs.adts.0.dump new file mode 100644 index 0000000000..e535aa8cd7 --- /dev/null +++ b/library/core/src/test/assets/ts/sample_cbs.adts.0.dump @@ -0,0 +1,631 @@ +seekMap: + isSeekable = true + duration = 3356772 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 0: + format: + bitrate = -1 + id = 0 + containerMimeType = null + sampleMimeType = audio/mp4a-latm + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 2, hash 5F7 + total output bytes = 30797 + sample count = 144 + sample 0: + time = 0 + flags = 1 + data = length 23, hash 47DE9131 + sample 1: + time = 23219 + flags = 1 + data = length 6, hash 31CF3A46 + sample 2: + time = 46438 + flags = 1 + data = length 6, hash 31CF3A46 + sample 3: + time = 69657 + flags = 1 + data = length 6, hash 31CF3A46 + sample 4: + time = 92876 + flags = 1 + data = length 6, hash 31EC5206 + sample 5: + time = 116095 + flags = 1 + data = length 171, hash 4F6478F6 + sample 6: + time = 139314 + flags = 1 + data = length 202, hash AF4068A3 + sample 7: + time = 162533 + flags = 1 + data = length 210, hash E4C10618 + sample 8: + time = 185752 + flags = 1 + data = length 217, hash 9ECCD0D9 + sample 9: + time = 208971 + flags = 1 + data = length 212, hash 6BAC2CD9 + sample 10: + time = 232190 + flags = 1 + data = length 223, hash 188B6010 + sample 11: + time = 255409 + flags = 1 + data = length 222, hash C1A04D0C + sample 12: + time = 278628 + flags = 1 + data = length 220, hash D65F9768 + sample 13: + time = 301847 + flags = 1 + data = length 227, hash B96C9E14 + sample 14: + time = 325066 + flags = 1 + data = length 229, hash 9FB09972 + sample 15: + time = 348285 + flags = 1 + data = length 220, hash 2271F053 + sample 16: + time = 371504 + flags = 1 + data = length 226, hash 5EDD2F4F + sample 17: + time = 394723 + flags = 1 + data = length 239, hash 957510E0 + sample 18: + time = 417942 + flags = 1 + data = length 224, hash 718A8F47 + sample 19: + time = 441161 + flags = 1 + data = length 225, hash 5E11E293 + sample 20: + time = 464380 + flags = 1 + data = length 227, hash FCE50D27 + sample 21: + time = 487599 + flags = 1 + data = length 212, hash 77908C40 + sample 22: + time = 510818 + flags = 1 + data = length 227, hash 34C4EB32 + sample 23: + time = 534037 + flags = 1 + data = length 231, hash 95488307 + sample 24: + time = 557256 + flags = 1 + data = length 226, hash 97F12D6F + sample 25: + time = 580475 + flags = 1 + data = length 236, hash 91A9D9A2 + sample 26: + time = 603694 + flags = 1 + data = length 227, hash 27A608F9 + sample 27: + time = 626913 + flags = 1 + data = length 229, hash 57DAAE4 + sample 28: + time = 650132 + flags = 1 + data = length 235, hash ED30AC34 + sample 29: + time = 673351 + flags = 1 + data = length 227, hash BD3D6280 + sample 30: + time = 696570 + flags = 1 + data = length 233, hash 694B1087 + sample 31: + time = 719789 + flags = 1 + data = length 232, hash 1EDFE047 + sample 32: + time = 743008 + flags = 1 + data = length 228, hash E2A831F4 + sample 33: + time = 766227 + flags = 1 + data = length 231, hash 757E6012 + sample 34: + time = 789446 + flags = 1 + data = length 223, hash 4003D791 + sample 35: + time = 812665 + flags = 1 + data = length 232, hash 3CF9A07C + sample 36: + time = 835884 + flags = 1 + data = length 228, hash 25AC3FF7 + sample 37: + time = 859103 + flags = 1 + data = length 220, hash 2C1824CE + sample 38: + time = 882322 + flags = 1 + data = length 229, hash 46FDD8FB + sample 39: + time = 905541 + flags = 1 + data = length 237, hash F6988018 + sample 40: + time = 928760 + flags = 1 + data = length 242, hash 60436B6B + sample 41: + time = 951979 + flags = 1 + data = length 275, hash 90EDFA8E + sample 42: + time = 975198 + flags = 1 + data = length 242, hash 5C86EFCB + sample 43: + time = 998417 + flags = 1 + data = length 233, hash E0A51B82 + sample 44: + time = 1021636 + flags = 1 + data = length 235, hash 590DF14F + sample 45: + time = 1044855 + flags = 1 + data = length 238, hash 69AF4E6E + sample 46: + time = 1068074 + flags = 1 + data = length 235, hash E745AE8D + sample 47: + time = 1091293 + flags = 1 + data = length 223, hash 295F2A13 + sample 48: + time = 1114512 + flags = 1 + data = length 228, hash E2F47B21 + sample 49: + time = 1137731 + flags = 1 + data = length 229, hash 262C3CFE + sample 50: + time = 1160950 + flags = 1 + data = length 232, hash 4B5BF5E8 + sample 51: + time = 1184169 + flags = 1 + data = length 233, hash F3D80836 + sample 52: + time = 1207388 + flags = 1 + data = length 237, hash 32E0A11E + sample 53: + time = 1230607 + flags = 1 + data = length 228, hash E1B89F13 + sample 54: + time = 1253826 + flags = 1 + data = length 237, hash 8BDD9E38 + sample 55: + time = 1277045 + flags = 1 + data = length 235, hash 3C84161F + sample 56: + time = 1300264 + flags = 1 + data = length 227, hash A47E1789 + sample 57: + time = 1323483 + flags = 1 + data = length 228, hash 869FDFD3 + sample 58: + time = 1346702 + flags = 1 + data = length 233, hash 272ECE2 + sample 59: + time = 1369921 + flags = 1 + data = length 227, hash DB6B9618 + sample 60: + time = 1393140 + flags = 1 + data = length 212, hash 63214325 + sample 61: + time = 1416359 + flags = 1 + data = length 221, hash 9BA588A1 + sample 62: + time = 1439578 + flags = 1 + data = length 225, hash 21EFD50C + sample 63: + time = 1462797 + flags = 1 + data = length 231, hash F3AD0BF + sample 64: + time = 1486016 + flags = 1 + data = length 224, hash 822C9210 + sample 65: + time = 1509235 + flags = 1 + data = length 195, hash D4EF53EE + sample 66: + time = 1532454 + flags = 1 + data = length 195, hash A816647A + sample 67: + time = 1555673 + flags = 1 + data = length 184, hash 9A2B7E6 + sample 68: + time = 1578892 + flags = 1 + data = length 210, hash 956E3600 + sample 69: + time = 1602111 + flags = 1 + data = length 234, hash 35CFDA0A + sample 70: + time = 1625330 + flags = 1 + data = length 239, hash 9E15AC1E + sample 71: + time = 1648549 + flags = 1 + data = length 228, hash F3B70641 + sample 72: + time = 1671768 + flags = 1 + data = length 237, hash 124E3194 + sample 73: + time = 1694987 + flags = 1 + data = length 231, hash 950CD7C8 + sample 74: + time = 1718206 + flags = 1 + data = length 236, hash A12E49AF + sample 75: + time = 1741425 + flags = 1 + data = length 242, hash 43BC9C24 + sample 76: + time = 1764644 + flags = 1 + data = length 241, hash DCF0B17 + sample 77: + time = 1787863 + flags = 1 + data = length 251, hash C0B99968 + sample 78: + time = 1811082 + flags = 1 + data = length 245, hash 9B38ED1C + sample 79: + time = 1834301 + flags = 1 + data = length 238, hash 1BA69079 + sample 80: + time = 1857520 + flags = 1 + data = length 233, hash 44C8C6BF + sample 81: + time = 1880739 + flags = 1 + data = length 231, hash EABBEE02 + sample 82: + time = 1903958 + flags = 1 + data = length 226, hash D09C44FB + sample 83: + time = 1927177 + flags = 1 + data = length 235, hash BE6A6608 + sample 84: + time = 1950396 + flags = 1 + data = length 235, hash 2735F454 + sample 85: + time = 1973615 + flags = 1 + data = length 238, hash B160DFE7 + sample 86: + time = 1996834 + flags = 1 + data = length 232, hash 1B217D2E + sample 87: + time = 2020053 + flags = 1 + data = length 251, hash D1C14CEA + sample 88: + time = 2043272 + flags = 1 + data = length 256, hash 97C87F08 + sample 89: + time = 2066491 + flags = 1 + data = length 237, hash 6645DB3 + sample 90: + time = 2089710 + flags = 1 + data = length 235, hash 727A1C82 + sample 91: + time = 2112929 + flags = 1 + data = length 234, hash 5015F8B5 + sample 92: + time = 2136148 + flags = 1 + data = length 241, hash 9102144B + sample 93: + time = 2159367 + flags = 1 + data = length 224, hash 64E0D807 + sample 94: + time = 2182586 + flags = 1 + data = length 228, hash 1922B852 + sample 95: + time = 2205805 + flags = 1 + data = length 224, hash 953502D8 + sample 96: + time = 2229024 + flags = 1 + data = length 214, hash 92B87FE7 + sample 97: + time = 2252243 + flags = 1 + data = length 213, hash BB0C8D86 + sample 98: + time = 2275462 + flags = 1 + data = length 206, hash 9AD21017 + sample 99: + time = 2298681 + flags = 1 + data = length 209, hash C479FE94 + sample 100: + time = 2321900 + flags = 1 + data = length 220, hash 3033DCE1 + sample 101: + time = 2345119 + flags = 1 + data = length 217, hash 7D589C94 + sample 102: + time = 2368338 + flags = 1 + data = length 216, hash AAF6C183 + sample 103: + time = 2391557 + flags = 1 + data = length 206, hash 1EE1207F + sample 104: + time = 2414776 + flags = 1 + data = length 204, hash 4BEB1210 + sample 105: + time = 2437995 + flags = 1 + data = length 213, hash 21A841C9 + sample 106: + time = 2461214 + flags = 1 + data = length 207, hash B80B0424 + sample 107: + time = 2484433 + flags = 1 + data = length 212, hash 4785A1C3 + sample 108: + time = 2507652 + flags = 1 + data = length 205, hash 59BF7229 + sample 109: + time = 2530871 + flags = 1 + data = length 208, hash FA313DDE + sample 110: + time = 2554090 + flags = 1 + data = length 211, hash 190D85FD + sample 111: + time = 2577309 + flags = 1 + data = length 211, hash BA050052 + sample 112: + time = 2600528 + flags = 1 + data = length 211, hash F3080F10 + sample 113: + time = 2623747 + flags = 1 + data = length 210, hash F41B7BE7 + sample 114: + time = 2646966 + flags = 1 + data = length 207, hash 2176C97E + sample 115: + time = 2670185 + flags = 1 + data = length 220, hash 32087455 + sample 116: + time = 2693404 + flags = 1 + data = length 213, hash 4E5649A8 + sample 117: + time = 2716623 + flags = 1 + data = length 213, hash 5F12FDCF + sample 118: + time = 2739842 + flags = 1 + data = length 204, hash 1E895C2A + sample 119: + time = 2763061 + flags = 1 + data = length 219, hash 45382270 + sample 120: + time = 2786280 + flags = 1 + data = length 205, hash D66C6A1D + sample 121: + time = 2809499 + flags = 1 + data = length 204, hash 467AD01F + sample 122: + time = 2832718 + flags = 1 + data = length 211, hash F0435574 + sample 123: + time = 2855937 + flags = 1 + data = length 206, hash 8C96B75F + sample 124: + time = 2879156 + flags = 1 + data = length 200, hash 82553248 + sample 125: + time = 2902375 + flags = 1 + data = length 180, hash 1E51E6CE + sample 126: + time = 2925594 + flags = 1 + data = length 196, hash 33151DC4 + sample 127: + time = 2948813 + flags = 1 + data = length 197, hash 1E62A7D6 + sample 128: + time = 2972032 + flags = 1 + data = length 206, hash 6A6C4CC9 + sample 129: + time = 2995251 + flags = 1 + data = length 209, hash A72FABAA + sample 130: + time = 3018470 + flags = 1 + data = length 217, hash BA33B985 + sample 131: + time = 3041689 + flags = 1 + data = length 235, hash 9919CFD9 + sample 132: + time = 3064908 + flags = 1 + data = length 236, hash A22C7267 + sample 133: + time = 3088127 + flags = 1 + data = length 213, hash 3D57C901 + sample 134: + time = 3111346 + flags = 1 + data = length 205, hash 47F68FDE + sample 135: + time = 3134565 + flags = 1 + data = length 210, hash 9A756E9C + sample 136: + time = 3157784 + flags = 1 + data = length 210, hash BD45C31F + sample 137: + time = 3181003 + flags = 1 + data = length 207, hash 8774FF7B + sample 138: + time = 3204222 + flags = 1 + data = length 149, hash 4678C0E5 + sample 139: + time = 3227441 + flags = 1 + data = length 161, hash E991035D + sample 140: + time = 3250660 + flags = 1 + data = length 197, hash C3013689 + sample 141: + time = 3273879 + flags = 1 + data = length 208, hash E6C0237 + sample 142: + time = 3297098 + flags = 1 + data = length 232, hash A330F188 + sample 143: + time = 3320317 + flags = 1 + data = length 174, hash 2B69C34E +track 1: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = application/id3 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 0 + sample count = 0 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample_cbs.adts.1.dump b/library/core/src/test/assets/ts/sample_cbs.adts.1.dump new file mode 100644 index 0000000000..96d2fcfb39 --- /dev/null +++ b/library/core/src/test/assets/ts/sample_cbs.adts.1.dump @@ -0,0 +1,431 @@ +seekMap: + isSeekable = true + duration = 3356772 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 0: + format: + bitrate = -1 + id = 0 + containerMimeType = null + sampleMimeType = audio/mp4a-latm + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 2, hash 5F7 + total output bytes = 20533 + sample count = 94 + sample 0: + time = 1118924 + flags = 1 + data = length 232, hash 4B5BF5E8 + sample 1: + time = 1142143 + flags = 1 + data = length 233, hash F3D80836 + sample 2: + time = 1165362 + flags = 1 + data = length 237, hash 32E0A11E + sample 3: + time = 1188581 + flags = 1 + data = length 228, hash E1B89F13 + sample 4: + time = 1211800 + flags = 1 + data = length 237, hash 8BDD9E38 + sample 5: + time = 1235019 + flags = 1 + data = length 235, hash 3C84161F + sample 6: + time = 1258238 + flags = 1 + data = length 227, hash A47E1789 + sample 7: + time = 1281457 + flags = 1 + data = length 228, hash 869FDFD3 + sample 8: + time = 1304676 + flags = 1 + data = length 233, hash 272ECE2 + sample 9: + time = 1327895 + flags = 1 + data = length 227, hash DB6B9618 + sample 10: + time = 1351114 + flags = 1 + data = length 212, hash 63214325 + sample 11: + time = 1374333 + flags = 1 + data = length 221, hash 9BA588A1 + sample 12: + time = 1397552 + flags = 1 + data = length 225, hash 21EFD50C + sample 13: + time = 1420771 + flags = 1 + data = length 231, hash F3AD0BF + sample 14: + time = 1443990 + flags = 1 + data = length 224, hash 822C9210 + sample 15: + time = 1467209 + flags = 1 + data = length 195, hash D4EF53EE + sample 16: + time = 1490428 + flags = 1 + data = length 195, hash A816647A + sample 17: + time = 1513647 + flags = 1 + data = length 184, hash 9A2B7E6 + sample 18: + time = 1536866 + flags = 1 + data = length 210, hash 956E3600 + sample 19: + time = 1560085 + flags = 1 + data = length 234, hash 35CFDA0A + sample 20: + time = 1583304 + flags = 1 + data = length 239, hash 9E15AC1E + sample 21: + time = 1606523 + flags = 1 + data = length 228, hash F3B70641 + sample 22: + time = 1629742 + flags = 1 + data = length 237, hash 124E3194 + sample 23: + time = 1652961 + flags = 1 + data = length 231, hash 950CD7C8 + sample 24: + time = 1676180 + flags = 1 + data = length 236, hash A12E49AF + sample 25: + time = 1699399 + flags = 1 + data = length 242, hash 43BC9C24 + sample 26: + time = 1722618 + flags = 1 + data = length 241, hash DCF0B17 + sample 27: + time = 1745837 + flags = 1 + data = length 251, hash C0B99968 + sample 28: + time = 1769056 + flags = 1 + data = length 245, hash 9B38ED1C + sample 29: + time = 1792275 + flags = 1 + data = length 238, hash 1BA69079 + sample 30: + time = 1815494 + flags = 1 + data = length 233, hash 44C8C6BF + sample 31: + time = 1838713 + flags = 1 + data = length 231, hash EABBEE02 + sample 32: + time = 1861932 + flags = 1 + data = length 226, hash D09C44FB + sample 33: + time = 1885151 + flags = 1 + data = length 235, hash BE6A6608 + sample 34: + time = 1908370 + flags = 1 + data = length 235, hash 2735F454 + sample 35: + time = 1931589 + flags = 1 + data = length 238, hash B160DFE7 + sample 36: + time = 1954808 + flags = 1 + data = length 232, hash 1B217D2E + sample 37: + time = 1978027 + flags = 1 + data = length 251, hash D1C14CEA + sample 38: + time = 2001246 + flags = 1 + data = length 256, hash 97C87F08 + sample 39: + time = 2024465 + flags = 1 + data = length 237, hash 6645DB3 + sample 40: + time = 2047684 + flags = 1 + data = length 235, hash 727A1C82 + sample 41: + time = 2070903 + flags = 1 + data = length 234, hash 5015F8B5 + sample 42: + time = 2094122 + flags = 1 + data = length 241, hash 9102144B + sample 43: + time = 2117341 + flags = 1 + data = length 224, hash 64E0D807 + sample 44: + time = 2140560 + flags = 1 + data = length 228, hash 1922B852 + sample 45: + time = 2163779 + flags = 1 + data = length 224, hash 953502D8 + sample 46: + time = 2186998 + flags = 1 + data = length 214, hash 92B87FE7 + sample 47: + time = 2210217 + flags = 1 + data = length 213, hash BB0C8D86 + sample 48: + time = 2233436 + flags = 1 + data = length 206, hash 9AD21017 + sample 49: + time = 2256655 + flags = 1 + data = length 209, hash C479FE94 + sample 50: + time = 2279874 + flags = 1 + data = length 220, hash 3033DCE1 + sample 51: + time = 2303093 + flags = 1 + data = length 217, hash 7D589C94 + sample 52: + time = 2326312 + flags = 1 + data = length 216, hash AAF6C183 + sample 53: + time = 2349531 + flags = 1 + data = length 206, hash 1EE1207F + sample 54: + time = 2372750 + flags = 1 + data = length 204, hash 4BEB1210 + sample 55: + time = 2395969 + flags = 1 + data = length 213, hash 21A841C9 + sample 56: + time = 2419188 + flags = 1 + data = length 207, hash B80B0424 + sample 57: + time = 2442407 + flags = 1 + data = length 212, hash 4785A1C3 + sample 58: + time = 2465626 + flags = 1 + data = length 205, hash 59BF7229 + sample 59: + time = 2488845 + flags = 1 + data = length 208, hash FA313DDE + sample 60: + time = 2512064 + flags = 1 + data = length 211, hash 190D85FD + sample 61: + time = 2535283 + flags = 1 + data = length 211, hash BA050052 + sample 62: + time = 2558502 + flags = 1 + data = length 211, hash F3080F10 + sample 63: + time = 2581721 + flags = 1 + data = length 210, hash F41B7BE7 + sample 64: + time = 2604940 + flags = 1 + data = length 207, hash 2176C97E + sample 65: + time = 2628159 + flags = 1 + data = length 220, hash 32087455 + sample 66: + time = 2651378 + flags = 1 + data = length 213, hash 4E5649A8 + sample 67: + time = 2674597 + flags = 1 + data = length 213, hash 5F12FDCF + sample 68: + time = 2697816 + flags = 1 + data = length 204, hash 1E895C2A + sample 69: + time = 2721035 + flags = 1 + data = length 219, hash 45382270 + sample 70: + time = 2744254 + flags = 1 + data = length 205, hash D66C6A1D + sample 71: + time = 2767473 + flags = 1 + data = length 204, hash 467AD01F + sample 72: + time = 2790692 + flags = 1 + data = length 211, hash F0435574 + sample 73: + time = 2813911 + flags = 1 + data = length 206, hash 8C96B75F + sample 74: + time = 2837130 + flags = 1 + data = length 200, hash 82553248 + sample 75: + time = 2860349 + flags = 1 + data = length 180, hash 1E51E6CE + sample 76: + time = 2883568 + flags = 1 + data = length 196, hash 33151DC4 + sample 77: + time = 2906787 + flags = 1 + data = length 197, hash 1E62A7D6 + sample 78: + time = 2930006 + flags = 1 + data = length 206, hash 6A6C4CC9 + sample 79: + time = 2953225 + flags = 1 + data = length 209, hash A72FABAA + sample 80: + time = 2976444 + flags = 1 + data = length 217, hash BA33B985 + sample 81: + time = 2999663 + flags = 1 + data = length 235, hash 9919CFD9 + sample 82: + time = 3022882 + flags = 1 + data = length 236, hash A22C7267 + sample 83: + time = 3046101 + flags = 1 + data = length 213, hash 3D57C901 + sample 84: + time = 3069320 + flags = 1 + data = length 205, hash 47F68FDE + sample 85: + time = 3092539 + flags = 1 + data = length 210, hash 9A756E9C + sample 86: + time = 3115758 + flags = 1 + data = length 210, hash BD45C31F + sample 87: + time = 3138977 + flags = 1 + data = length 207, hash 8774FF7B + sample 88: + time = 3162196 + flags = 1 + data = length 149, hash 4678C0E5 + sample 89: + time = 3185415 + flags = 1 + data = length 161, hash E991035D + sample 90: + time = 3208634 + flags = 1 + data = length 197, hash C3013689 + sample 91: + time = 3231853 + flags = 1 + data = length 208, hash E6C0237 + sample 92: + time = 3255072 + flags = 1 + data = length 232, hash A330F188 + sample 93: + time = 3278291 + flags = 1 + data = length 174, hash 2B69C34E +track 1: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = application/id3 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 0 + sample count = 0 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample_cbs.adts.2.dump b/library/core/src/test/assets/ts/sample_cbs.adts.2.dump new file mode 100644 index 0000000000..2e581bca28 --- /dev/null +++ b/library/core/src/test/assets/ts/sample_cbs.adts.2.dump @@ -0,0 +1,251 @@ +seekMap: + isSeekable = true + duration = 3356772 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 0: + format: + bitrate = -1 + id = 0 + containerMimeType = null + sampleMimeType = audio/mp4a-latm + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 2, hash 5F7 + total output bytes = 10161 + sample count = 49 + sample 0: + time = 2237848 + flags = 1 + data = length 224, hash 953502D8 + sample 1: + time = 2261067 + flags = 1 + data = length 214, hash 92B87FE7 + sample 2: + time = 2284286 + flags = 1 + data = length 213, hash BB0C8D86 + sample 3: + time = 2307505 + flags = 1 + data = length 206, hash 9AD21017 + sample 4: + time = 2330724 + flags = 1 + data = length 209, hash C479FE94 + sample 5: + time = 2353943 + flags = 1 + data = length 220, hash 3033DCE1 + sample 6: + time = 2377162 + flags = 1 + data = length 217, hash 7D589C94 + sample 7: + time = 2400381 + flags = 1 + data = length 216, hash AAF6C183 + sample 8: + time = 2423600 + flags = 1 + data = length 206, hash 1EE1207F + sample 9: + time = 2446819 + flags = 1 + data = length 204, hash 4BEB1210 + sample 10: + time = 2470038 + flags = 1 + data = length 213, hash 21A841C9 + sample 11: + time = 2493257 + flags = 1 + data = length 207, hash B80B0424 + sample 12: + time = 2516476 + flags = 1 + data = length 212, hash 4785A1C3 + sample 13: + time = 2539695 + flags = 1 + data = length 205, hash 59BF7229 + sample 14: + time = 2562914 + flags = 1 + data = length 208, hash FA313DDE + sample 15: + time = 2586133 + flags = 1 + data = length 211, hash 190D85FD + sample 16: + time = 2609352 + flags = 1 + data = length 211, hash BA050052 + sample 17: + time = 2632571 + flags = 1 + data = length 211, hash F3080F10 + sample 18: + time = 2655790 + flags = 1 + data = length 210, hash F41B7BE7 + sample 19: + time = 2679009 + flags = 1 + data = length 207, hash 2176C97E + sample 20: + time = 2702228 + flags = 1 + data = length 220, hash 32087455 + sample 21: + time = 2725447 + flags = 1 + data = length 213, hash 4E5649A8 + sample 22: + time = 2748666 + flags = 1 + data = length 213, hash 5F12FDCF + sample 23: + time = 2771885 + flags = 1 + data = length 204, hash 1E895C2A + sample 24: + time = 2795104 + flags = 1 + data = length 219, hash 45382270 + sample 25: + time = 2818323 + flags = 1 + data = length 205, hash D66C6A1D + sample 26: + time = 2841542 + flags = 1 + data = length 204, hash 467AD01F + sample 27: + time = 2864761 + flags = 1 + data = length 211, hash F0435574 + sample 28: + time = 2887980 + flags = 1 + data = length 206, hash 8C96B75F + sample 29: + time = 2911199 + flags = 1 + data = length 200, hash 82553248 + sample 30: + time = 2934418 + flags = 1 + data = length 180, hash 1E51E6CE + sample 31: + time = 2957637 + flags = 1 + data = length 196, hash 33151DC4 + sample 32: + time = 2980856 + flags = 1 + data = length 197, hash 1E62A7D6 + sample 33: + time = 3004075 + flags = 1 + data = length 206, hash 6A6C4CC9 + sample 34: + time = 3027294 + flags = 1 + data = length 209, hash A72FABAA + sample 35: + time = 3050513 + flags = 1 + data = length 217, hash BA33B985 + sample 36: + time = 3073732 + flags = 1 + data = length 235, hash 9919CFD9 + sample 37: + time = 3096951 + flags = 1 + data = length 236, hash A22C7267 + sample 38: + time = 3120170 + flags = 1 + data = length 213, hash 3D57C901 + sample 39: + time = 3143389 + flags = 1 + data = length 205, hash 47F68FDE + sample 40: + time = 3166608 + flags = 1 + data = length 210, hash 9A756E9C + sample 41: + time = 3189827 + flags = 1 + data = length 210, hash BD45C31F + sample 42: + time = 3213046 + flags = 1 + data = length 207, hash 8774FF7B + sample 43: + time = 3236265 + flags = 1 + data = length 149, hash 4678C0E5 + sample 44: + time = 3259484 + flags = 1 + data = length 161, hash E991035D + sample 45: + time = 3282703 + flags = 1 + data = length 197, hash C3013689 + sample 46: + time = 3305922 + flags = 1 + data = length 208, hash E6C0237 + sample 47: + time = 3329141 + flags = 1 + data = length 232, hash A330F188 + sample 48: + time = 3352360 + flags = 1 + data = length 174, hash 2B69C34E +track 1: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = application/id3 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 0 + sample count = 0 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample_cbs.adts.3.dump b/library/core/src/test/assets/ts/sample_cbs.adts.3.dump new file mode 100644 index 0000000000..e134a711bf --- /dev/null +++ b/library/core/src/test/assets/ts/sample_cbs.adts.3.dump @@ -0,0 +1,59 @@ +seekMap: + isSeekable = true + duration = 3356772 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 0: + format: + bitrate = -1 + id = 0 + containerMimeType = null + sampleMimeType = audio/mp4a-latm + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 2, hash 5F7 + total output bytes = 174 + sample count = 1 + sample 0: + time = 3356772 + flags = 1 + data = length 174, hash 2B69C34E +track 1: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = application/id3 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 0 + sample count = 0 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample_cbs.adts.unklen.dump b/library/core/src/test/assets/ts/sample_cbs.adts.unklen.dump new file mode 100644 index 0000000000..93d7b776c0 --- /dev/null +++ b/library/core/src/test/assets/ts/sample_cbs.adts.unklen.dump @@ -0,0 +1,631 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 0: + format: + bitrate = -1 + id = 0 + containerMimeType = null + sampleMimeType = audio/mp4a-latm + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 2, hash 5F7 + total output bytes = 30797 + sample count = 144 + sample 0: + time = 0 + flags = 1 + data = length 23, hash 47DE9131 + sample 1: + time = 23219 + flags = 1 + data = length 6, hash 31CF3A46 + sample 2: + time = 46438 + flags = 1 + data = length 6, hash 31CF3A46 + sample 3: + time = 69657 + flags = 1 + data = length 6, hash 31CF3A46 + sample 4: + time = 92876 + flags = 1 + data = length 6, hash 31EC5206 + sample 5: + time = 116095 + flags = 1 + data = length 171, hash 4F6478F6 + sample 6: + time = 139314 + flags = 1 + data = length 202, hash AF4068A3 + sample 7: + time = 162533 + flags = 1 + data = length 210, hash E4C10618 + sample 8: + time = 185752 + flags = 1 + data = length 217, hash 9ECCD0D9 + sample 9: + time = 208971 + flags = 1 + data = length 212, hash 6BAC2CD9 + sample 10: + time = 232190 + flags = 1 + data = length 223, hash 188B6010 + sample 11: + time = 255409 + flags = 1 + data = length 222, hash C1A04D0C + sample 12: + time = 278628 + flags = 1 + data = length 220, hash D65F9768 + sample 13: + time = 301847 + flags = 1 + data = length 227, hash B96C9E14 + sample 14: + time = 325066 + flags = 1 + data = length 229, hash 9FB09972 + sample 15: + time = 348285 + flags = 1 + data = length 220, hash 2271F053 + sample 16: + time = 371504 + flags = 1 + data = length 226, hash 5EDD2F4F + sample 17: + time = 394723 + flags = 1 + data = length 239, hash 957510E0 + sample 18: + time = 417942 + flags = 1 + data = length 224, hash 718A8F47 + sample 19: + time = 441161 + flags = 1 + data = length 225, hash 5E11E293 + sample 20: + time = 464380 + flags = 1 + data = length 227, hash FCE50D27 + sample 21: + time = 487599 + flags = 1 + data = length 212, hash 77908C40 + sample 22: + time = 510818 + flags = 1 + data = length 227, hash 34C4EB32 + sample 23: + time = 534037 + flags = 1 + data = length 231, hash 95488307 + sample 24: + time = 557256 + flags = 1 + data = length 226, hash 97F12D6F + sample 25: + time = 580475 + flags = 1 + data = length 236, hash 91A9D9A2 + sample 26: + time = 603694 + flags = 1 + data = length 227, hash 27A608F9 + sample 27: + time = 626913 + flags = 1 + data = length 229, hash 57DAAE4 + sample 28: + time = 650132 + flags = 1 + data = length 235, hash ED30AC34 + sample 29: + time = 673351 + flags = 1 + data = length 227, hash BD3D6280 + sample 30: + time = 696570 + flags = 1 + data = length 233, hash 694B1087 + sample 31: + time = 719789 + flags = 1 + data = length 232, hash 1EDFE047 + sample 32: + time = 743008 + flags = 1 + data = length 228, hash E2A831F4 + sample 33: + time = 766227 + flags = 1 + data = length 231, hash 757E6012 + sample 34: + time = 789446 + flags = 1 + data = length 223, hash 4003D791 + sample 35: + time = 812665 + flags = 1 + data = length 232, hash 3CF9A07C + sample 36: + time = 835884 + flags = 1 + data = length 228, hash 25AC3FF7 + sample 37: + time = 859103 + flags = 1 + data = length 220, hash 2C1824CE + sample 38: + time = 882322 + flags = 1 + data = length 229, hash 46FDD8FB + sample 39: + time = 905541 + flags = 1 + data = length 237, hash F6988018 + sample 40: + time = 928760 + flags = 1 + data = length 242, hash 60436B6B + sample 41: + time = 951979 + flags = 1 + data = length 275, hash 90EDFA8E + sample 42: + time = 975198 + flags = 1 + data = length 242, hash 5C86EFCB + sample 43: + time = 998417 + flags = 1 + data = length 233, hash E0A51B82 + sample 44: + time = 1021636 + flags = 1 + data = length 235, hash 590DF14F + sample 45: + time = 1044855 + flags = 1 + data = length 238, hash 69AF4E6E + sample 46: + time = 1068074 + flags = 1 + data = length 235, hash E745AE8D + sample 47: + time = 1091293 + flags = 1 + data = length 223, hash 295F2A13 + sample 48: + time = 1114512 + flags = 1 + data = length 228, hash E2F47B21 + sample 49: + time = 1137731 + flags = 1 + data = length 229, hash 262C3CFE + sample 50: + time = 1160950 + flags = 1 + data = length 232, hash 4B5BF5E8 + sample 51: + time = 1184169 + flags = 1 + data = length 233, hash F3D80836 + sample 52: + time = 1207388 + flags = 1 + data = length 237, hash 32E0A11E + sample 53: + time = 1230607 + flags = 1 + data = length 228, hash E1B89F13 + sample 54: + time = 1253826 + flags = 1 + data = length 237, hash 8BDD9E38 + sample 55: + time = 1277045 + flags = 1 + data = length 235, hash 3C84161F + sample 56: + time = 1300264 + flags = 1 + data = length 227, hash A47E1789 + sample 57: + time = 1323483 + flags = 1 + data = length 228, hash 869FDFD3 + sample 58: + time = 1346702 + flags = 1 + data = length 233, hash 272ECE2 + sample 59: + time = 1369921 + flags = 1 + data = length 227, hash DB6B9618 + sample 60: + time = 1393140 + flags = 1 + data = length 212, hash 63214325 + sample 61: + time = 1416359 + flags = 1 + data = length 221, hash 9BA588A1 + sample 62: + time = 1439578 + flags = 1 + data = length 225, hash 21EFD50C + sample 63: + time = 1462797 + flags = 1 + data = length 231, hash F3AD0BF + sample 64: + time = 1486016 + flags = 1 + data = length 224, hash 822C9210 + sample 65: + time = 1509235 + flags = 1 + data = length 195, hash D4EF53EE + sample 66: + time = 1532454 + flags = 1 + data = length 195, hash A816647A + sample 67: + time = 1555673 + flags = 1 + data = length 184, hash 9A2B7E6 + sample 68: + time = 1578892 + flags = 1 + data = length 210, hash 956E3600 + sample 69: + time = 1602111 + flags = 1 + data = length 234, hash 35CFDA0A + sample 70: + time = 1625330 + flags = 1 + data = length 239, hash 9E15AC1E + sample 71: + time = 1648549 + flags = 1 + data = length 228, hash F3B70641 + sample 72: + time = 1671768 + flags = 1 + data = length 237, hash 124E3194 + sample 73: + time = 1694987 + flags = 1 + data = length 231, hash 950CD7C8 + sample 74: + time = 1718206 + flags = 1 + data = length 236, hash A12E49AF + sample 75: + time = 1741425 + flags = 1 + data = length 242, hash 43BC9C24 + sample 76: + time = 1764644 + flags = 1 + data = length 241, hash DCF0B17 + sample 77: + time = 1787863 + flags = 1 + data = length 251, hash C0B99968 + sample 78: + time = 1811082 + flags = 1 + data = length 245, hash 9B38ED1C + sample 79: + time = 1834301 + flags = 1 + data = length 238, hash 1BA69079 + sample 80: + time = 1857520 + flags = 1 + data = length 233, hash 44C8C6BF + sample 81: + time = 1880739 + flags = 1 + data = length 231, hash EABBEE02 + sample 82: + time = 1903958 + flags = 1 + data = length 226, hash D09C44FB + sample 83: + time = 1927177 + flags = 1 + data = length 235, hash BE6A6608 + sample 84: + time = 1950396 + flags = 1 + data = length 235, hash 2735F454 + sample 85: + time = 1973615 + flags = 1 + data = length 238, hash B160DFE7 + sample 86: + time = 1996834 + flags = 1 + data = length 232, hash 1B217D2E + sample 87: + time = 2020053 + flags = 1 + data = length 251, hash D1C14CEA + sample 88: + time = 2043272 + flags = 1 + data = length 256, hash 97C87F08 + sample 89: + time = 2066491 + flags = 1 + data = length 237, hash 6645DB3 + sample 90: + time = 2089710 + flags = 1 + data = length 235, hash 727A1C82 + sample 91: + time = 2112929 + flags = 1 + data = length 234, hash 5015F8B5 + sample 92: + time = 2136148 + flags = 1 + data = length 241, hash 9102144B + sample 93: + time = 2159367 + flags = 1 + data = length 224, hash 64E0D807 + sample 94: + time = 2182586 + flags = 1 + data = length 228, hash 1922B852 + sample 95: + time = 2205805 + flags = 1 + data = length 224, hash 953502D8 + sample 96: + time = 2229024 + flags = 1 + data = length 214, hash 92B87FE7 + sample 97: + time = 2252243 + flags = 1 + data = length 213, hash BB0C8D86 + sample 98: + time = 2275462 + flags = 1 + data = length 206, hash 9AD21017 + sample 99: + time = 2298681 + flags = 1 + data = length 209, hash C479FE94 + sample 100: + time = 2321900 + flags = 1 + data = length 220, hash 3033DCE1 + sample 101: + time = 2345119 + flags = 1 + data = length 217, hash 7D589C94 + sample 102: + time = 2368338 + flags = 1 + data = length 216, hash AAF6C183 + sample 103: + time = 2391557 + flags = 1 + data = length 206, hash 1EE1207F + sample 104: + time = 2414776 + flags = 1 + data = length 204, hash 4BEB1210 + sample 105: + time = 2437995 + flags = 1 + data = length 213, hash 21A841C9 + sample 106: + time = 2461214 + flags = 1 + data = length 207, hash B80B0424 + sample 107: + time = 2484433 + flags = 1 + data = length 212, hash 4785A1C3 + sample 108: + time = 2507652 + flags = 1 + data = length 205, hash 59BF7229 + sample 109: + time = 2530871 + flags = 1 + data = length 208, hash FA313DDE + sample 110: + time = 2554090 + flags = 1 + data = length 211, hash 190D85FD + sample 111: + time = 2577309 + flags = 1 + data = length 211, hash BA050052 + sample 112: + time = 2600528 + flags = 1 + data = length 211, hash F3080F10 + sample 113: + time = 2623747 + flags = 1 + data = length 210, hash F41B7BE7 + sample 114: + time = 2646966 + flags = 1 + data = length 207, hash 2176C97E + sample 115: + time = 2670185 + flags = 1 + data = length 220, hash 32087455 + sample 116: + time = 2693404 + flags = 1 + data = length 213, hash 4E5649A8 + sample 117: + time = 2716623 + flags = 1 + data = length 213, hash 5F12FDCF + sample 118: + time = 2739842 + flags = 1 + data = length 204, hash 1E895C2A + sample 119: + time = 2763061 + flags = 1 + data = length 219, hash 45382270 + sample 120: + time = 2786280 + flags = 1 + data = length 205, hash D66C6A1D + sample 121: + time = 2809499 + flags = 1 + data = length 204, hash 467AD01F + sample 122: + time = 2832718 + flags = 1 + data = length 211, hash F0435574 + sample 123: + time = 2855937 + flags = 1 + data = length 206, hash 8C96B75F + sample 124: + time = 2879156 + flags = 1 + data = length 200, hash 82553248 + sample 125: + time = 2902375 + flags = 1 + data = length 180, hash 1E51E6CE + sample 126: + time = 2925594 + flags = 1 + data = length 196, hash 33151DC4 + sample 127: + time = 2948813 + flags = 1 + data = length 197, hash 1E62A7D6 + sample 128: + time = 2972032 + flags = 1 + data = length 206, hash 6A6C4CC9 + sample 129: + time = 2995251 + flags = 1 + data = length 209, hash A72FABAA + sample 130: + time = 3018470 + flags = 1 + data = length 217, hash BA33B985 + sample 131: + time = 3041689 + flags = 1 + data = length 235, hash 9919CFD9 + sample 132: + time = 3064908 + flags = 1 + data = length 236, hash A22C7267 + sample 133: + time = 3088127 + flags = 1 + data = length 213, hash 3D57C901 + sample 134: + time = 3111346 + flags = 1 + data = length 205, hash 47F68FDE + sample 135: + time = 3134565 + flags = 1 + data = length 210, hash 9A756E9C + sample 136: + time = 3157784 + flags = 1 + data = length 210, hash BD45C31F + sample 137: + time = 3181003 + flags = 1 + data = length 207, hash 8774FF7B + sample 138: + time = 3204222 + flags = 1 + data = length 149, hash 4678C0E5 + sample 139: + time = 3227441 + flags = 1 + data = length 161, hash E991035D + sample 140: + time = 3250660 + flags = 1 + data = length 197, hash C3013689 + sample 141: + time = 3273879 + flags = 1 + data = length 208, hash E6C0237 + sample 142: + time = 3297098 + flags = 1 + data = length 232, hash A330F188 + sample 143: + time = 3320317 + flags = 1 + data = length 174, hash 2B69C34E +track 1: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = application/id3 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 0 + sample count = 0 +tracksEnded = true diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 0df854cddb..1e676f2123 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -18,20 +18,25 @@ package com.google.android.exoplayer2; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import android.content.Context; +import android.support.annotation.Nullable; import android.view.Surface; -import com.google.android.exoplayer2.Player.DefaultEventListener; +import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.testutil.ActionSchedule; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; +import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; @@ -44,17 +49,24 @@ import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinit import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; import com.google.android.exoplayer2.testutil.RobolectricUtil; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Clock; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; /** Unit test for {@link ExoPlayer}. */ @@ -69,6 +81,13 @@ public final class ExoPlayerTest { */ private static final int TIMEOUT_MS = 10000; + private Context context; + + @Before + public void setUp() { + context = RuntimeEnvironment.application; + } + /** * Tests playback of a source that exposes an empty timeline. Playback is expected to end without * error. @@ -81,7 +100,7 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setRenderers(renderer) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); @@ -102,7 +121,7 @@ public final class ExoPlayerTest { .setTimeline(timeline) .setManifest(manifest) .setRenderers(renderer) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); @@ -124,7 +143,7 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setRenderers(renderer) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertPositionDiscontinuityReasonsEqual( @@ -148,7 +167,7 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setRenderers(renderer) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); Integer[] expectedReasons = new Integer[99]; @@ -205,7 +224,7 @@ public final class ExoPlayerTest { .setTimeline(timeline) .setRenderers(videoRenderer, audioRenderer) .setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertPositionDiscontinuityReasonsEqual( @@ -230,8 +249,10 @@ public final class ExoPlayerTest { new FakeMediaSource(timeline, new Object(), Builder.VIDEO_FORMAT) { @Override public synchronized void prepareSourceInternal( - ExoPlayer player, boolean isTopLevelSource) { - super.prepareSourceInternal(player, isTopLevelSource); + ExoPlayer player, + boolean isTopLevelSource, + @Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(player, isTopLevelSource, mediaTransferListener); // We've queued a source info refresh on the playback thread's event queue. Allow the // test thread to prepare the player with the third source, and block this thread (the // playback thread) until the test thread's call to prepare() has returned. @@ -280,7 +301,7 @@ public final class ExoPlayerTest { .setMediaSource(firstSource) .setRenderers(renderer) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); @@ -325,7 +346,7 @@ public final class ExoPlayerTest { .setTimeline(timeline) .setRenderers(renderer) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertPlayedPeriodIndices(0, 1, 1, 2, 2, 0, 0, 0, 1, 2); @@ -372,7 +393,7 @@ public final class ExoPlayerTest { .setMediaSource(mediaSource) .setRenderers(renderer) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertPlayedPeriodIndices(0, 1, 0, 2, 1, 2); @@ -429,7 +450,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setMediaSource(fakeMediaSource) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); // There is still one discontinuity from content to content for the failed ad insertion. @@ -450,7 +471,7 @@ public final class ExoPlayerTest { new Builder() .setRenderers(renderer) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); assertThat(renderer.isEnded).isTrue(); @@ -483,7 +504,7 @@ public final class ExoPlayerTest { .build(); final List playbackStatesWhenSeekProcessed = new ArrayList<>(); EventListener eventListener = - new DefaultEventListener() { + new EventListener() { private int currentPlaybackState = Player.STATE_IDLE; @Override @@ -501,7 +522,7 @@ public final class ExoPlayerTest { .setTimeline(timeline) .setEventListener(eventListener) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertPositionDiscontinuityReasonsEqual( @@ -535,19 +556,20 @@ public final class ExoPlayerTest { .build(); final boolean[] onSeekProcessedCalled = new boolean[1]; EventListener listener = - new DefaultEventListener() { + new EventListener() { @Override public void onSeekProcessed() { onSeekProcessedCalled[0] = true; } }; ExoPlayerTestRunner testRunner = - new Builder().setActionSchedule(actionSchedule).setEventListener(listener).build(); + new Builder().setActionSchedule(actionSchedule).setEventListener(listener).build(context); try { testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); fail(); } catch (ExoPlaybackException e) { // Expected exception. + assertThat(e.getUnexpectedException()).isInstanceOf(IllegalSeekPositionException.class); } assertThat(onSeekProcessedCalled[0]).isTrue(); } @@ -561,7 +583,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); @@ -577,7 +599,8 @@ public final class ExoPlayerTest { MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator, - EventDispatcher eventDispatcher) { + EventDispatcher eventDispatcher, + @Nullable TransferListener transferListener) { FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray, eventDispatcher); mediaPeriod.setSeekToUsOffset(10); return mediaPeriod; @@ -594,7 +617,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertPositionDiscontinuityReasonsEqual( @@ -611,7 +634,8 @@ public final class ExoPlayerTest { MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator, - EventDispatcher eventDispatcher) { + EventDispatcher eventDispatcher, + @Nullable TransferListener transferListener) { FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray, eventDispatcher); mediaPeriod.setDiscontinuityPositionUs(10); return mediaPeriod; @@ -620,7 +644,7 @@ public final class ExoPlayerTest { ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setMediaSource(mediaSource) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_INTERNAL); @@ -636,7 +660,8 @@ public final class ExoPlayerTest { MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator, - EventDispatcher eventDispatcher) { + EventDispatcher eventDispatcher, + @Nullable TransferListener transferListener) { FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray, eventDispatcher); mediaPeriod.setDiscontinuityPositionUs(0); return mediaPeriod; @@ -645,7 +670,7 @@ public final class ExoPlayerTest { ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setMediaSource(mediaSource) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); // If the position is unchanged we do not expect the discontinuity to be reported externally. @@ -665,7 +690,7 @@ public final class ExoPlayerTest { .setMediaSource(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); @@ -694,7 +719,7 @@ public final class ExoPlayerTest { .setMediaSource(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); @@ -732,7 +757,7 @@ public final class ExoPlayerTest { .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .setActionSchedule(disableTrackAction) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); @@ -771,7 +796,7 @@ public final class ExoPlayerTest { .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .setActionSchedule(disableTrackAction) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); @@ -812,7 +837,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertTimelinesEqual(timeline1, timeline2); @@ -853,7 +878,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setMediaSource(firstMediaSource) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertPlayedPeriodIndices(0, 1, 0); @@ -872,7 +897,8 @@ public final class ExoPlayerTest { MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator, - EventDispatcher eventDispatcher) { + EventDispatcher eventDispatcher, + @Nullable TransferListener transferListener) { // Defer completing preparation of the period until playback parameters have been set. fakeMediaPeriodHolder[0] = new FakeMediaPeriod(trackGroupArray, eventDispatcher, /* deferOnPrepared= */ true); @@ -909,7 +935,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); } @@ -936,7 +962,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); @@ -968,7 +994,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); @@ -1000,7 +1026,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); @@ -1025,7 +1051,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilActionScheduleFinished(TIMEOUT_MS); mediaSource.assertReleased(); @@ -1046,7 +1072,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilActionScheduleFinished(TIMEOUT_MS); mediaSource.assertReleased(); @@ -1069,7 +1095,7 @@ public final class ExoPlayerTest { .setTimeline(timeline) .setActionSchedule(actionSchedule) .setExpectedPlayerEndedCount(2) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline); @@ -1099,7 +1125,7 @@ public final class ExoPlayerTest { .setTimeline(timeline) .setActionSchedule(actionSchedule) .setExpectedPlayerEndedCount(2) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline); @@ -1125,7 +1151,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); @@ -1151,7 +1177,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); @@ -1178,7 +1204,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(); + .build(context); try { testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); fail(); @@ -1227,7 +1253,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(); + .build(context); try { testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); fail(); @@ -1267,17 +1293,102 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) - .build(); + .build(context); try { testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); fail(); } catch (ExoPlaybackException e) { // Expected exception. + assertThat(e.getUnexpectedException()).isInstanceOf(IllegalSeekPositionException.class); } testRunner.assertTimelinesEqual(timeline); testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); } + @Test + public void testPlaybackErrorDuringSourceInfoRefreshWithShuffleModeEnabledUsesCorrectFirstPeriod() + throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); + ConcatenatingMediaSource concatenatingMediaSource = + new ConcatenatingMediaSource( + /* isAtomic= */ false, new FakeShuffleOrder(0), mediaSource, mediaSource); + AtomicInteger windowIndexAfterReprepare = new AtomicInteger(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testPlaybackErrorDuringSourceInfoRefreshUsesCorrectFirstPeriod") + .setShuffleModeEnabled(true) + .waitForPlaybackState(Player.STATE_BUFFERING) + // Cause an internal exception by seeking to an invalid position while the media source + // is still being prepared. The error will be thrown while the player handles the new + // source info. + .seek(/* windowIndex= */ 100, /* positionMs= */ 0) + .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null)) + .waitForPlaybackState(Player.STATE_IDLE) + // Re-prepare to play the source in its default shuffled order. + .prepareSource( + concatenatingMediaSource, /* resetPosition= */ false, /* resetState= */ false) + .waitForTimelineChanged(null) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + windowIndexAfterReprepare.set(player.getCurrentWindowIndex()); + } + }) + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setMediaSource(concatenatingMediaSource) + .setActionSchedule(actionSchedule) + .build(context); + try { + testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + fail(); + } catch (ExoPlaybackException e) { + // Expected exception. + assertThat(e.getUnexpectedException()).isInstanceOf(IllegalSeekPositionException.class); + } + assertThat(windowIndexAfterReprepare.get()).isEqualTo(1); + } + + @Test + public void testRestartAfterEmptyTimelineWithShuffleModeEnabledUsesCorrectFirstPeriod() + throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + FakeMediaSource mediaSource = new FakeMediaSource(timeline, /* manifest= */ null); + ConcatenatingMediaSource concatenatingMediaSource = + new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); + AtomicInteger windowIndexAfterAddingSources = new AtomicInteger(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testRestartAfterEmptyTimelineUsesCorrectFirstPeriod") + .setShuffleModeEnabled(true) + // Preparing with an empty media source will transition to ended state. + .waitForPlaybackState(Player.STATE_ENDED) + // Add two sources at once such that the default start position in the shuffled order + // will be the second source. + .executeRunnable( + () -> + concatenatingMediaSource.addMediaSources( + Arrays.asList(mediaSource, mediaSource))) + .waitForTimelineChanged(null) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + windowIndexAfterAddingSources.set(player.getCurrentWindowIndex()); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(concatenatingMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertThat(windowIndexAfterAddingSources.get()).isEqualTo(1); + } + @Test public void testPlaybackErrorAndReprepareDoesNotResetPosition() throws Exception { final Timeline timeline = new FakeTimeline(/* windowCount= */ 2); @@ -1329,7 +1440,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(); + .build(context); try { testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); fail(); @@ -1364,7 +1475,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build(); + .build(context); try { testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); fail(); @@ -1390,10 +1501,10 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); - assertThat(target.positionMs >= 50).isTrue(); + assertThat(target.positionMs).isAtLeast(50L); } @Test @@ -1410,10 +1521,10 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); - assertThat(target.positionMs >= 50).isTrue(); + assertThat(target.positionMs).isAtLeast(50L); } @Test @@ -1432,11 +1543,11 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); - assertThat(target50.positionMs >= 50).isTrue(); - assertThat(target80.positionMs >= 80).isTrue(); + assertThat(target50.positionMs).isAtLeast(50L); + assertThat(target80.positionMs).isAtLeast(80L); assertThat(target80.positionMs).isAtLeast(target50.positionMs); } @@ -1456,11 +1567,11 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); - assertThat(target1.positionMs >= 50).isTrue(); - assertThat(target2.positionMs >= 50).isTrue(); + assertThat(target1.positionMs).isAtLeast(50L); + assertThat(target2.positionMs).isAtLeast(50L); } @Test @@ -1478,10 +1589,10 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); - assertThat(target.positionMs >= 50).isTrue(); + assertThat(target.positionMs).isAtLeast(50L); } @Test @@ -1515,7 +1626,7 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); @@ -1542,10 +1653,10 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); - assertThat(target.positionMs >= 50).isTrue(); + assertThat(target.positionMs).isAtLeast(50L); } @Test @@ -1562,10 +1673,10 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); - assertThat(target.positionMs >= 50).isTrue(); + assertThat(target.positionMs).isAtLeast(50L); } @Test @@ -1583,7 +1694,7 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.positionMs).isEqualTo(C.POSITION_UNSET); @@ -1604,7 +1715,7 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.positionMs).isEqualTo(C.POSITION_UNSET); @@ -1627,11 +1738,11 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.messageCount).isEqualTo(1); - assertThat(target.positionMs >= 50).isTrue(); + assertThat(target.positionMs).isAtLeast(50L); } @Test @@ -1656,11 +1767,11 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.messageCount).isEqualTo(2); - assertThat(target.positionMs >= 50).isTrue(); + assertThat(target.positionMs).isAtLeast(50L); } @Test @@ -1691,10 +1802,10 @@ public final class ExoPlayerTest { new Builder() .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); - assertThat(target.positionMs >= 50).isTrue(); + assertThat(target.positionMs).isAtLeast(50L); assertThat(target.windowIndex).isEqualTo(1); } @@ -1712,11 +1823,11 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.windowIndex).isEqualTo(2); - assertThat(target.positionMs >= 50).isTrue(); + assertThat(target.positionMs).isAtLeast(50L); } @Test @@ -1733,11 +1844,11 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target.windowIndex).isEqualTo(2); - assertThat(target.positionMs >= 50).isTrue(); + assertThat(target.positionMs).isAtLeast(50L); } @Test @@ -1771,10 +1882,10 @@ public final class ExoPlayerTest { new Builder() .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); - assertThat(target.positionMs >= 50).isTrue(); + assertThat(target.positionMs).isAtLeast(50L); assertThat(target.windowIndex).isEqualTo(0); } @@ -1805,7 +1916,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); assertThat(target1.windowIndex).isEqualTo(0); @@ -1844,7 +1955,7 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); assertThat(message.get().isCanceled()).isTrue(); @@ -1888,7 +1999,7 @@ public final class ExoPlayerTest { new Builder() .setTimeline(timeline) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); assertThat(message.get().isCanceled()).isTrue(); @@ -1911,7 +2022,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setRenderers(videoRenderer) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); @@ -1927,7 +2038,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setTimeline(Timeline.EMPTY) .setActionSchedule(waitForEndedAndSwitchSchedule) - .build() + .build(context) .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); @@ -1967,7 +2078,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder() .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) - .build() + .build(context) .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertPlayedPeriodIndices(0, 1); @@ -1980,6 +2091,259 @@ public final class ExoPlayerTest { .inOrder(); } + @Test + public void testRepeatedSeeksToUnpreparedPeriodInSameWindowKeepsWindowSequenceNumber() + throws Exception { + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 2, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ 10 * C.MICROS_PER_SECOND)); + FakeMediaSource mediaSource = new FakeMediaSource(timeline, /* manifest= */ null); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSeekToUnpreparedPeriod") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .seek(/* windowIndex= */ 0, /* positionMs= */ 9999) + .seek(/* windowIndex= */ 0, /* positionMs= */ 1) + .seek(/* windowIndex= */ 0, /* positionMs= */ 9999) + .play() + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + + testRunner.assertPlayedPeriodIndices(0, 1, 0, 1); + assertThat(mediaSource.getCreatedMediaPeriods()) + .containsAllOf( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0)); + assertThat(mediaSource.getCreatedMediaPeriods()) + .doesNotContain(new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1)); + } + + @Test + public void testRecursivePlayerChangesReportConsistentValuesForAllListeners() throws Exception { + // We add two listeners to the player. The first stops the player as soon as it's ready and both + // record the state change events they receive. + final AtomicReference playerReference = new AtomicReference<>(); + final List eventListener1States = new ArrayList<>(); + final List eventListener2States = new ArrayList<>(); + final EventListener eventListener1 = + new EventListener() { + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + eventListener1States.add(playbackState); + if (playbackState == Player.STATE_READY) { + playerReference.get().stop(/* reset= */ true); + } + } + }; + final EventListener eventListener2 = + new EventListener() { + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + eventListener2States.add(playbackState); + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testRecursivePlayerChanges") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + playerReference.set(player); + player.addListener(eventListener1); + player.addListener(eventListener2); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(eventListener1States) + .containsExactly(Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_IDLE) + .inOrder(); + assertThat(eventListener2States) + .containsExactly(Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_IDLE) + .inOrder(); + } + + @Test + public void testRecursivePlayerChangesAreReportedInCorrectOrder() throws Exception { + // The listener stops the player as soon as it's ready (which should report a timeline and state + // change) and sets playWhenReady to false when the timeline callback is received. + final AtomicReference playerReference = new AtomicReference<>(); + final List eventListenerPlayWhenReady = new ArrayList<>(); + final List eventListenerStates = new ArrayList<>(); + final EventListener eventListener = + new EventListener() { + @Override + public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) { + if (timeline.isEmpty()) { + playerReference.get().setPlayWhenReady(/* playWhenReady= */ false); + } + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + eventListenerPlayWhenReady.add(playWhenReady); + eventListenerStates.add(playbackState); + if (playbackState == Player.STATE_READY) { + playerReference.get().stop(/* reset= */ true); + } + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testRecursivePlayerChanges") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + playerReference.set(player); + player.addListener(eventListener); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(eventListenerStates) + .containsExactly( + Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_IDLE, Player.STATE_IDLE) + .inOrder(); + assertThat(eventListenerPlayWhenReady).containsExactly(true, true, true, false).inOrder(); + } + + @Test + public void testClippedLoopedPeriodsArePlayedFully() throws Exception { + long startPositionUs = 300_000; + long expectedDurationUs = 700_000; + MediaSource mediaSource = + new ClippingMediaSource( + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null), + startPositionUs, + startPositionUs + expectedDurationUs); + Clock clock = new AutoAdvancingFakeClock(); + AtomicReference playerReference = new AtomicReference<>(); + AtomicReference positionAtDiscontinuityMs = new AtomicReference<>(); + AtomicReference clockAtStartMs = new AtomicReference<>(); + AtomicReference clockAtDiscontinuityMs = new AtomicReference<>(); + EventListener eventListener = + new EventListener() { + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (playbackState == Player.STATE_READY && clockAtStartMs.get() == null) { + clockAtStartMs.set(clock.elapsedRealtime()); + } + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) { + positionAtDiscontinuityMs.set(playerReference.get().getCurrentPosition()); + clockAtDiscontinuityMs.set(clock.elapsedRealtime()); + } + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testClippedLoopedPeriodsArePlayedFully") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + playerReference.set(player); + player.addListener(eventListener); + } + }) + .pause() + .setRepeatMode(Player.REPEAT_MODE_ALL) + .waitForPlaybackState(Player.STATE_READY) + // Play until the media repeats once. + .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 1) + .playUntilStartOfWindow(/* windowIndex= */ 0) + .setRepeatMode(Player.REPEAT_MODE_OFF) + .play() + .build(); + new ExoPlayerTestRunner.Builder() + .setClock(clock) + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(positionAtDiscontinuityMs.get()).isAtLeast(0L); + assertThat(clockAtDiscontinuityMs.get() - clockAtStartMs.get()) + .isAtLeast(C.usToMs(expectedDurationUs)); + } + + @Test + public void testUpdateTrackSelectorThenSeekToUnpreparedPeriod_returnsEmptyTrackGroups() + throws Exception { + Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource[] fakeMediaSources = { + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, null, Builder.AUDIO_FORMAT) + }; + MediaSource mediaSource = + new ConcatenatingMediaSource( + /* isAtomic= */ false, + /* useLazyPreparation= */ true, + new ShuffleOrder.DefaultShuffleOrder(0), + fakeMediaSources); + FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .seek(/* windowIndex= */ 1, /* positionMs= */ 0) + .play() + .build(); + List trackGroupsList = new ArrayList<>(); + List trackSelectionsList = new ArrayList<>(); + new Builder() + .setMediaSource(mediaSource) + .setTrackSelector(trackSelector) + .setRenderers(renderer) + .setActionSchedule(actionSchedule) + .setEventListener( + new EventListener() { + @Override + public void onTracksChanged( + TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + trackGroupsList.add(trackGroups); + trackSelectionsList.add(trackSelections); + } + }) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + assertThat(trackGroupsList).hasSize(3); + // First track groups of the 1st period are reported. + // Then the seek to an unprepared period will result in empty track groups and selections being + // returned. + // Then the track groups of the 2nd period are reported. + assertThat(trackGroupsList.get(0).get(0).getFormat(0)).isEqualTo(Builder.VIDEO_FORMAT); + assertThat(trackGroupsList.get(1)).isEqualTo(TrackGroupArray.EMPTY); + assertThat(trackSelectionsList.get(1).get(0)).isNull(); + assertThat(trackGroupsList.get(2).get(0).getFormat(0)).isEqualTo(Builder.AUDIO_FORMAT); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java b/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java index 7ca2181ebf..33e6ed0838 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java @@ -64,10 +64,35 @@ public final class FormatTest { ColorInfo colorInfo = new ColorInfo(C.COLOR_SPACE_BT709, C.COLOR_RANGE_LIMITED, C.COLOR_TRANSFER_SDR, new byte[] {1, 2, 3, 4, 5, 6, 7}); - Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, - 1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, colorInfo, 6, - 44100, C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.NO_VALUE, - Format.OFFSET_SAMPLE_RELATIVE, INIT_DATA, drmInitData, metadata); + Format formatToParcel = + new Format( + "id", + "label", + /* containerMimeType= */ MimeTypes.VIDEO_MP4, + /* sampleMimeType= */ MimeTypes.VIDEO_H264, + "codec", + /* bitrate= */ 1024, + /* maxInputSize= */ 2048, + /* width= */ 1920, + /* height= */ 1080, + /* frameRate= */ 24, + /* rotationDegrees= */ 90, + /* pixelWidthHeightRatio= */ 2, + projectionData, + C.STEREO_MODE_TOP_BOTTOM, + colorInfo, + /* channelCount= */ 6, + /* sampleRate= */ 44100, + C.ENCODING_PCM_24BIT, + /* encoderDelay= */ 1001, + /* encoderPadding= */ 1002, + C.SELECTION_FLAG_DEFAULT, + "language", + /* accessibilityChannel= */ Format.NO_VALUE, + Format.OFFSET_SAMPLE_RELATIVE, + INIT_DATA, + drmInitData, + metadata); Parcel parcel = Parcel.obtain(); formatToParcel.writeToParcel(parcel, 0); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index 623506ad0d..3216087169 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -93,8 +93,7 @@ public final class AnalyticsCollectorTest { private static final int EVENT_MEDIA_PERIOD_RELEASED = 18; private static final int EVENT_READING_STARTED = 19; private static final int EVENT_BANDWIDTH_ESTIMATE = 20; - private static final int EVENT_VIEWPORT_SIZE_CHANGED = 21; - private static final int EVENT_NETWORK_TYPE_CHANGED = 22; + private static final int EVENT_SURFACE_SIZE_CHANGED = 21; private static final int EVENT_METADATA = 23; private static final int EVENT_DECODER_ENABLED = 24; private static final int EVENT_DECODER_INIT = 25; @@ -671,10 +670,6 @@ public final class AnalyticsCollectorTest { new PlayerRunnable() { @Override public void run(SimpleExoPlayer player) { - player.getAnalyticsCollector().notifyNetworkTypeChanged(networkInfo); - player - .getAnalyticsCollector() - .notifyViewportSizeChanged(/* width= */ 320, /* height= */ 240); player.getAnalyticsCollector().notifySeekStarted(); } }) @@ -685,8 +680,6 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(PERIOD_0); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(PERIOD_0); - assertThat(listener.getEvents(EVENT_VIEWPORT_SIZE_CHANGED)).containsExactly(PERIOD_0); - assertThat(listener.getEvents(EVENT_NETWORK_TYPE_CHANGED)).containsExactly(PERIOD_0); } private static TestAnalyticsListener runAnalyticsTest(MediaSource mediaSource) throws Exception { @@ -718,7 +711,7 @@ public final class AnalyticsCollectorTest { .setRenderersFactory(renderersFactory) .setAnalyticsListener(listener) .setActionSchedule(actionSchedule) - .build() + .build(RuntimeEnvironment.application) .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); @@ -829,7 +822,7 @@ public final class AnalyticsCollectorTest { @Override protected void onBufferRead() { if (!notifiedAudioSessionId) { - eventDispatcher.audioSessionId(/* audioSessionId= */ 0); + eventDispatcher.audioSessionId(/* audioSessionId= */ 1); notifiedAudioSessionId = true; } } @@ -1017,13 +1010,8 @@ public final class AnalyticsCollectorTest { } @Override - public void onViewportSizeChange(EventTime eventTime, int width, int height) { - reportedEvents.add(new ReportedEvent(EVENT_VIEWPORT_SIZE_CHANGED, eventTime)); - } - - @Override - public void onNetworkTypeChanged(EventTime eventTime, @Nullable NetworkInfo networkInfo) { - reportedEvents.add(new ReportedEvent(EVENT_NETWORK_TYPE_CHANGED, eventTime)); + public void onSurfaceSizeChanged(EventTime eventTime, int width, int height) { + reportedEvents.add(new ReportedEvent(EVENT_SURFACE_SIZE_CHANGED, eventTime)); } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/Ac3UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/Ac3UtilTest.java new file mode 100644 index 0000000000..e41e270966 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/Ac3UtilTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.audio; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.util.Util; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit tests for {@link Ac3Util}. */ +@RunWith(RobolectricTestRunner.class) +public final class Ac3UtilTest { + + private static final int TRUEHD_SYNCFRAME_SAMPLE_COUNT = 40; + private static final byte[] TRUEHD_SYNCFRAME_HEADER = + Util.getBytesFromHexString("C07504D8F8726FBA0097C00FB7520000"); + private static final byte[] TRUEHD_NON_SYNCFRAME_HEADER = + Util.getBytesFromHexString("A025048860224E6F6DEDB6D5B6DBAFE6"); + + @Test + public void testParseTrueHdSyncframeAudioSampleCount_nonSyncframe() { + assertThat(Ac3Util.parseTrueHdSyncframeAudioSampleCount(TRUEHD_NON_SYNCFRAME_HEADER)) + .isEqualTo(0); + } + + @Test + public void testParseTrueHdSyncframeAudioSampleCount_syncframe() { + assertThat(Ac3Util.parseTrueHdSyncframeAudioSampleCount(TRUEHD_SYNCFRAME_HEADER)) + .isEqualTo(TRUEHD_SYNCFRAME_SAMPLE_COUNT); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java index 115862074d..04de9a76f4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java @@ -210,8 +210,8 @@ public final class SilenceSkippingAudioProcessorTest { process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE); // The right number of frames are skipped/output. - assertThat(totalOutputFrames).isEqualTo(53990); - assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(46010); + assertThat(totalOutputFrames).isEqualTo(57980); + assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(42020); } @Test @@ -240,8 +240,8 @@ public final class SilenceSkippingAudioProcessorTest { process(silenceSkippingAudioProcessor, inputBufferProvider, /* inputBufferSize= */ 80); // The right number of frames are skipped/output. - assertThat(totalOutputFrames).isEqualTo(53990); - assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(46010); + assertThat(totalOutputFrames).isEqualTo(57980); + assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(42020); } @Test @@ -270,8 +270,8 @@ public final class SilenceSkippingAudioProcessorTest { process(silenceSkippingAudioProcessor, inputBufferProvider, /* inputBufferSize= */ 120); // The right number of frames are skipped/output. - assertThat(totalOutputFrames).isEqualTo(53990); - assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(46010); + assertThat(totalOutputFrames).isEqualTo(57980); + assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(42020); } @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMapTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMapTest.java new file mode 100644 index 0000000000..0fa33dd348 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMapTest.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.exoplayer2.extractor; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.C; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test for {@link ConstantBitrateSeekMap}. */ +@RunWith(RobolectricTestRunner.class) +public final class ConstantBitrateSeekMapTest { + + private ConstantBitrateSeekMap constantBitrateSeekMap; + + @Test + public void testIsSeekable_forKnownInputLength_returnSeekable() { + constantBitrateSeekMap = + new ConstantBitrateSeekMap( + /* inputLength= */ 1000, + /* firstFrameBytePosition= */ 0, + /* bitrate= */ 8_000, + /* frameSize= */ 100); + assertThat(constantBitrateSeekMap.isSeekable()).isTrue(); + } + + @Test + public void testIsSeekable_forUnknownInputLength_returnUnseekable() { + constantBitrateSeekMap = + new ConstantBitrateSeekMap( + /* inputLength= */ C.LENGTH_UNSET, + /* firstFrameBytePosition= */ 0, + /* bitrate= */ 8_000, + /* frameSize= */ 100); + assertThat(constantBitrateSeekMap.isSeekable()).isFalse(); + } + + @Test + public void testGetSeekPoints_forUnseekableInput_returnSeekPoint0() { + int firstBytePosition = 100; + constantBitrateSeekMap = + new ConstantBitrateSeekMap( + /* inputLength= */ C.LENGTH_UNSET, + /* firstFrameBytePosition= */ firstBytePosition, + /* bitrate= */ 8_000, + /* frameSize= */ 100); + SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 123); + assertThat(seekPoints.first.timeUs).isEqualTo(0); + assertThat(seekPoints.first.position).isEqualTo(firstBytePosition); + assertThat(seekPoints.second).isEqualTo(seekPoints.first); + } + + @Test + public void testGetDurationUs_forKnownInputLength_returnCorrectDuration() { + constantBitrateSeekMap = + new ConstantBitrateSeekMap( + /* inputLength= */ 2_300, + /* firstFrameBytePosition= */ 100, + /* bitrate= */ 8_000, + /* frameSize= */ 100); + // Bitrate = 8000 (bits/s) = 1000 (bytes/s) + // FrameSize = 100 (bytes), so 1 frame = 1s = 100_000 us + // Input length = 2300 (bytes), first frame = 100, so duration = 2_200_000 us. + assertThat(constantBitrateSeekMap.getDurationUs()).isEqualTo(2_200_000); + } + + @Test + public void testGetDurationUs_forUnnnownInputLength_returnUnknownDuration() { + constantBitrateSeekMap = + new ConstantBitrateSeekMap( + /* inputLength= */ C.LENGTH_UNSET, + /* firstFrameBytePosition= */ 100, + /* bitrate= */ 8_000, + /* frameSize= */ 100); + assertThat(constantBitrateSeekMap.getDurationUs()).isEqualTo(C.TIME_UNSET); + } + + @Test + public void testGetSeekPoints_forSeekableInput_forSyncPosition0_return1SeekPoint() { + int firstBytePosition = 100; + constantBitrateSeekMap = + new ConstantBitrateSeekMap( + /* inputLength= */ 2_300, + /* firstFrameBytePosition= */ firstBytePosition, + /* bitrate= */ 8_000, + /* frameSize= */ 100); + SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 0); + assertThat(seekPoints.first.timeUs).isEqualTo(0); + assertThat(seekPoints.first.position).isEqualTo(firstBytePosition); + assertThat(seekPoints.second).isEqualTo(seekPoints.first); + } + + @Test + public void testGetSeekPoints_forSeekableInput_forSeekPointAtSyncPosition_return1SeekPoint() { + constantBitrateSeekMap = + new ConstantBitrateSeekMap( + /* inputLength= */ 2_300, + /* firstFrameBytePosition= */ 100, + /* bitrate= */ 8_000, + /* frameSize= */ 100); + SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 1_200_000); + // Bitrate = 8000 (bits/s) = 1000 (bytes/s) + // FrameSize = 100 (bytes), so 1 frame = 1s = 100_000 us + assertThat(seekPoints.first.timeUs).isEqualTo(1_200_000); + assertThat(seekPoints.first.position).isEqualTo(1300); + assertThat(seekPoints.second).isEqualTo(seekPoints.first); + } + + @Test + public void testGetSeekPoints_forSeekableInput_forNonSyncSeekPosition_return2SeekPoints() { + constantBitrateSeekMap = + new ConstantBitrateSeekMap( + /* inputLength= */ 2_300, + /* firstFrameBytePosition= */ 100, + /* bitrate= */ 8_000, + /* frameSize= */ 100); + SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 345_678); + // Bitrate = 8000 (bits/s) = 1000 (bytes/s) + // FrameSize = 100 (bytes), so 1 frame = 1s = 100_000 us + assertThat(seekPoints.first.timeUs).isEqualTo(300_000); + assertThat(seekPoints.first.position).isEqualTo(400); + assertThat(seekPoints.second.timeUs).isEqualTo(400_000); + assertThat(seekPoints.second.position).isEqualTo(500); + } + + @Test + public void testGetSeekPoints_forSeekableInput_forSeekPointWithinLastFrame_return1SeekPoint() { + constantBitrateSeekMap = + new ConstantBitrateSeekMap( + /* inputLength= */ 2_300, + /* firstFrameBytePosition= */ 100, + /* bitrate= */ 8_000, + /* frameSize= */ 100); + SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 2_123_456); + assertThat(seekPoints.first.timeUs).isEqualTo(2_100_000); + assertThat(seekPoints.first.position).isEqualTo(2_200); + assertThat(seekPoints.second).isEqualTo(seekPoints.first); + } + + @Test + public void testGetSeekPoints_forSeekableInput_forSeekPointAtEndOfStream_return1SeekPoint() { + constantBitrateSeekMap = + new ConstantBitrateSeekMap( + /* inputLength= */ 2_300, + /* firstFrameBytePosition= */ 100, + /* bitrate= */ 8_000, + /* frameSize= */ 100); + SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 2_200_000); + assertThat(seekPoints.first.timeUs).isEqualTo(2_100_000); + assertThat(seekPoints.first.position).isEqualTo(2_200); + assertThat(seekPoints.second).isEqualTo(seekPoints.first); + } + + @Test + public void testGetTimeUsAtPosition_forPosition0_return0() { + constantBitrateSeekMap = + new ConstantBitrateSeekMap( + /* inputLength= */ 2_300, + /* firstFrameBytePosition= */ 100, + /* bitrate= */ 8_000, + /* frameSize= */ 100); + long timeUs = constantBitrateSeekMap.getTimeUsAtPosition(0); + assertThat(timeUs).isEqualTo(0); + } + + @Test + public void testGetTimeUsAtPosition_forPositionWithinStream_returnCorrectTime() { + constantBitrateSeekMap = + new ConstantBitrateSeekMap( + /* inputLength= */ 2_300, + /* firstFrameBytePosition= */ 100, + /* bitrate= */ 8_000, + /* frameSize= */ 100); + long timeUs = constantBitrateSeekMap.getTimeUsAtPosition(1234); + assertThat(timeUs).isEqualTo(1_134_000); + } + + @Test + public void testGetTimeUsAtPosition_forPositionAtEndOfStream_returnStreamDuration() { + constantBitrateSeekMap = + new ConstantBitrateSeekMap( + /* inputLength= */ 2_300, + /* firstFrameBytePosition= */ 100, + /* bitrate= */ 8_000, + /* frameSize= */ 100); + long timeUs = constantBitrateSeekMap.getTimeUsAtPosition(2300); + assertThat(timeUs).isEqualTo(constantBitrateSeekMap.getDurationUs()); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorSeekTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorSeekTest.java new file mode 100644 index 0000000000..9f9051087d --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorSeekTest.java @@ -0,0 +1,410 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.amr; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.testutil.FakeExtractorOutput; +import com.google.android.exoplayer2.testutil.FakeTrackOutput; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.DefaultDataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import java.io.IOException; +import java.util.List; +import java.util.Random; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +/** Unit test for {@link AmrExtractor}. */ +@RunWith(RobolectricTestRunner.class) +public final class AmrExtractorSeekTest { + + private static final Random random = new Random(1234L); + + private static final String NARROW_BAND_AMR_FILE = "amr/sample_nb.amr"; + private static final int NARROW_BAND_FILE_DURATION_US = 4_360_000; + + private static final String WIDE_BAND_AMR_FILE = "amr/sample_wb.amr"; + private static final int WIDE_BAND_FILE_DURATION_US = 3_380_000; + + private FakeTrackOutput expectedTrackOutput; + private DefaultDataSource dataSource; + + @Before + public void setUp() { + dataSource = + new DefaultDataSourceFactory(RuntimeEnvironment.application, "UserAgent") + .createDataSource(); + } + + @Test + public void testAmrExtractorReads_returnSeekableSeekMap_forNarrowBandAmr() + throws IOException, InterruptedException { + String fileName = NARROW_BAND_AMR_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAmrExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + + AmrExtractor extractor = createAmrExtractor(); + SeekMap seekMap = + TestUtil.extractSeekMap(extractor, new FakeExtractorOutput(), dataSource, fileUri); + + assertThat(seekMap).isNotNull(); + assertThat(seekMap.getDurationUs()).isEqualTo(NARROW_BAND_FILE_DURATION_US); + assertThat(seekMap.isSeekable()).isTrue(); + } + + @Test + public void testSeeking_handlesSeekingToPositionInFile_extractsCorrectFrame_forNarrowBandAmr() + throws IOException, InterruptedException { + String fileName = NARROW_BAND_AMR_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAmrExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + + AmrExtractor extractor = createAmrExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long targetSeekTimeUs = 980_000; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void testSeeking_handlesSeekToEoF_extractsLastFrame_forNarrowBandAmr() + throws IOException, InterruptedException { + String fileName = NARROW_BAND_AMR_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAmrExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + AmrExtractor extractor = createAmrExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long targetSeekTimeUs = seekMap.getDurationUs(); + + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void testSeeking_handlesSeekingBackward_extractsCorrectFrames_forNarrowBandAmr() + throws IOException, InterruptedException { + String fileName = NARROW_BAND_AMR_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAmrExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + AmrExtractor extractor = createAmrExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long firstSeekTimeUs = 980_000; + TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri); + + long targetSeekTimeUs = 0; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void testSeeking_handlesSeekingForward_extractsCorrectFrames_forNarrowBandAmr() + throws IOException, InterruptedException { + String fileName = NARROW_BAND_AMR_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAmrExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + AmrExtractor extractor = createAmrExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long firstSeekTimeUs = 980_000; + TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri); + + long targetSeekTimeUs = 1_200_000; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void testSeeking_handlesRandomSeeks_extractsCorrectFrames_forNarrowBandAmr() + throws IOException, InterruptedException { + String fileName = NARROW_BAND_AMR_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAmrExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + AmrExtractor extractor = createAmrExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long numSeek = 100; + for (long i = 0; i < numSeek; i++) { + long targetSeekTimeUs = random.nextInt(NARROW_BAND_FILE_DURATION_US + 1); + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + } + + @Test + public void testAmrExtractorReads_returnSeekableSeekMap_forWideBandAmr() + throws IOException, InterruptedException { + String fileName = WIDE_BAND_AMR_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAmrExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + + AmrExtractor extractor = createAmrExtractor(); + SeekMap seekMap = + TestUtil.extractSeekMap(extractor, new FakeExtractorOutput(), dataSource, fileUri); + + assertThat(seekMap).isNotNull(); + assertThat(seekMap.getDurationUs()).isEqualTo(WIDE_BAND_FILE_DURATION_US); + assertThat(seekMap.isSeekable()).isTrue(); + } + + @Test + public void testSeeking_handlesSeekingToPositionInFile_extractsCorrectFrame_forWideBandAmr() + throws IOException, InterruptedException { + String fileName = WIDE_BAND_AMR_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAmrExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + + AmrExtractor extractor = createAmrExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long targetSeekTimeUs = 980_000; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void testSeeking_handlesSeekToEoF_extractsLastFrame_forWideBandAmr() + throws IOException, InterruptedException { + String fileName = WIDE_BAND_AMR_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAmrExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + AmrExtractor extractor = createAmrExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long targetSeekTimeUs = seekMap.getDurationUs(); + + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void testSeeking_handlesSeekingBackward_extractsCorrectFrames_forWideBandAmr() + throws IOException, InterruptedException { + String fileName = WIDE_BAND_AMR_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAmrExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + AmrExtractor extractor = createAmrExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long firstSeekTimeUs = 980_000; + TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri); + + long targetSeekTimeUs = 0; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void testSeeking_handlesSeekingForward_extractsCorrectFrames_forWideBandAmr() + throws IOException, InterruptedException { + String fileName = WIDE_BAND_AMR_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAmrExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + AmrExtractor extractor = createAmrExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long firstSeekTimeUs = 980_000; + TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri); + + long targetSeekTimeUs = 1_200_000; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void testSeeking_handlesRandomSeeks_extractsCorrectFrames_forWideBandAmr() + throws IOException, InterruptedException { + String fileName = WIDE_BAND_AMR_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAmrExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + AmrExtractor extractor = createAmrExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long numSeek = 100; + for (long i = 0; i < numSeek; i++) { + long targetSeekTimeUs = random.nextInt(NARROW_BAND_FILE_DURATION_US + 1); + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + } + + // Internal methods + + private AmrExtractor createAmrExtractor() { + return new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING); + } + + private void assertFirstFrameAfterSeekContainTargetSeekTime( + FakeTrackOutput trackOutput, long seekTimeUs, int firstFrameIndexAfterSeek) { + int expectedSampleIndex = findTargetFrameInExpectedOutput(seekTimeUs); + // Assert that after seeking, the first sample frame written to output contains the sample + // at seek time. + trackOutput.assertSample( + firstFrameIndexAfterSeek, + expectedTrackOutput.getSampleData(expectedSampleIndex), + expectedTrackOutput.getSampleTimeUs(expectedSampleIndex), + expectedTrackOutput.getSampleFlags(expectedSampleIndex), + expectedTrackOutput.getSampleCryptoData(expectedSampleIndex)); + } + + private int findTargetFrameInExpectedOutput(long seekTimeUs) { + List sampleTimes = expectedTrackOutput.getSampleTimesUs(); + for (int i = 0; i < sampleTimes.size() - 1; i++) { + long currentSampleTime = sampleTimes.get(i); + long nextSampleTime = sampleTimes.get(i + 1); + if (currentSampleTime <= seekTimeUs && nextSampleTime > seekTimeUs) { + return i; + } + } + return sampleTimes.size() - 1; + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorTest.java index b46612e7c3..39c1bfe05b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorTest.java @@ -179,12 +179,26 @@ public final class AmrExtractorTest { @Test public void testExtractingNarrowBandSamples() throws Exception { - ExtractorAsserts.assertBehavior(createAmrExtractorFactory(), "amr/sample_nb.amr"); + ExtractorAsserts.assertBehavior( + createAmrExtractorFactory(/* withSeeking= */ false), "amr/sample_nb.amr"); } @Test public void testExtractingWideBandSamples() throws Exception { - ExtractorAsserts.assertBehavior(createAmrExtractorFactory(), "amr/sample_wb.amr"); + ExtractorAsserts.assertBehavior( + createAmrExtractorFactory(/* withSeeking= */ false), "amr/sample_wb.amr"); + } + + @Test + public void testExtractingNarrowBandSamples_withSeeking() throws Exception { + ExtractorAsserts.assertBehavior( + createAmrExtractorFactory(/* withSeeking= */ true), "amr/sample_nb_cbr.amr"); + } + + @Test + public void testExtractingWideBandSamples_withSeeking() throws Exception { + ExtractorAsserts.assertBehavior( + createAmrExtractorFactory(/* withSeeking= */ true), "amr/sample_wb_cbr.amr"); } private byte[] newWideBandAmrFrameWithType(int frameType) { @@ -235,11 +249,15 @@ public final class AmrExtractorTest { } @NonNull - private static ExtractorAsserts.ExtractorFactory createAmrExtractorFactory() { + private static ExtractorAsserts.ExtractorFactory createAmrExtractorFactory(boolean withSeeking) { return new ExtractorAsserts.ExtractorFactory() { @Override public Extractor create() { - return new AmrExtractor(); + if (!withSeeking) { + return new AmrExtractor(); + } else { + return new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING); + } } }; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java index 176211acb8..8662434f81 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java @@ -33,13 +33,13 @@ public final class FragmentedMp4ExtractorTest { @Test public void testSample() throws Exception { ExtractorAsserts.assertBehavior( - getExtractorFactory(Collections.emptyList()), "mp4/sample_fragmented.mp4"); + getExtractorFactory(Collections.emptyList()), "mp4/sample_fragmented.mp4"); } @Test public void testSampleSeekable() throws Exception { ExtractorAsserts.assertBehavior( - getExtractorFactory(Collections.emptyList()), "mp4/sample_fragmented_seekable.mp4"); + getExtractorFactory(Collections.emptyList()), "mp4/sample_fragmented_seekable.mp4"); } @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java index 9632577e82..565d609842 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java @@ -38,14 +38,15 @@ public final class RawCcExtractorTest { public Extractor create() { return new RawCcExtractor( Format.createTextContainerFormat( - null, - null, - MimeTypes.APPLICATION_CEA608, - "cea608", - Format.NO_VALUE, - 0, - null, - 1)); + /* id= */ null, + /* label= */ null, + /* containerMimeType= */ null, + /* sampleMimeType= */ MimeTypes.APPLICATION_CEA608, + /* codecs= */ "cea608", + /* bitrate= */ Format.NO_VALUE, + /* selectionFlags= */ 0, + /* language= */ null, + /* accessibilityChannel= */ 1)); } }, "rawcc/sample.rawcc"); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java new file mode 100644 index 0000000000..c0a35427b0 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.ts; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.testutil.FakeExtractorOutput; +import com.google.android.exoplayer2.testutil.FakeTrackOutput; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.DefaultDataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import java.io.IOException; +import java.util.Arrays; +import java.util.Random; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +/** Unit test for {@link AdtsExtractor}. */ +@RunWith(RobolectricTestRunner.class) +public final class AdtsExtractorSeekTest { + + private static final Random random = new Random(1234L); + + private static final String TEST_FILE = "ts/sample.adts"; + private static final int FILE_DURATION_US = 3_356_772; + private static final long DELTA_TIMESTAMP_THRESHOLD_US = 200_000; + + private FakeTrackOutput expectedTrackOutput; + private DefaultDataSource dataSource; + + @Before + public void setUp() { + dataSource = + new DefaultDataSourceFactory(RuntimeEnvironment.application, "UserAgent") + .createDataSource(); + } + + @Test + public void testAdtsExtractorReads_returnSeekableSeekMap() + throws IOException, InterruptedException { + String fileName = TEST_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAdtsExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + + AdtsExtractor extractor = createAdtsExtractor(); + SeekMap seekMap = + TestUtil.extractSeekMap(extractor, new FakeExtractorOutput(), dataSource, fileUri); + + assertThat(seekMap).isNotNull(); + assertThat(seekMap.getDurationUs()).isEqualTo(FILE_DURATION_US); + assertThat(seekMap.isSeekable()).isTrue(); + } + + @Test + public void testSeeking_handlesSeekingToPositionInFile_extractsCorrectSample() + throws IOException, InterruptedException { + String fileName = TEST_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAdtsExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + + AdtsExtractor extractor = createAdtsExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long targetSeekTimeUs = 980_000; + int extractedSampleIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedSampleIndex).isNotEqualTo(-1); + assertFirstSampleAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedSampleIndex); + } + + @Test + public void testSeeking_handlesSeekToEoF_extractsLastSample() + throws IOException, InterruptedException { + String fileName = TEST_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAdtsExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + AdtsExtractor extractor = createAdtsExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long targetSeekTimeUs = seekMap.getDurationUs(); + + int extractedSampleIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedSampleIndex).isNotEqualTo(-1); + assertFirstSampleAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedSampleIndex); + } + + @Test + public void testSeeking_handlesSeekingBackward_extractsCorrectSamples() + throws IOException, InterruptedException { + String fileName = TEST_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAdtsExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + AdtsExtractor extractor = createAdtsExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long firstSeekTimeUs = 980_000; + TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri); + + long targetSeekTimeUs = 0; + int extractedSampleIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedSampleIndex).isNotEqualTo(-1); + assertFirstSampleAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedSampleIndex); + } + + @Test + public void testSeeking_handlesSeekingForward_extractsCorrectSamples() + throws IOException, InterruptedException { + String fileName = TEST_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAdtsExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + AdtsExtractor extractor = createAdtsExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long firstSeekTimeUs = 980_000; + TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri); + + long targetSeekTimeUs = 1_200_000; + int extractedSampleIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedSampleIndex).isNotEqualTo(-1); + assertFirstSampleAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedSampleIndex); + } + + @Test + public void testSeeking_handlesRandomSeeks_extractsCorrectSamples() + throws IOException, InterruptedException { + String fileName = TEST_FILE; + Uri fileUri = TestUtil.buildAssetUri(fileName); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + createAdtsExtractor(), RuntimeEnvironment.application, fileName) + .trackOutputs + .get(0); + AdtsExtractor extractor = createAdtsExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long numSeek = 100; + for (long i = 0; i < numSeek; i++) { + long targetSeekTimeUs = random.nextInt(FILE_DURATION_US + 1); + int extractedSampleIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedSampleIndex).isNotEqualTo(-1); + assertFirstSampleAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedSampleIndex); + } + } + + // Internal methods + + private static AdtsExtractor createAdtsExtractor() { + return new AdtsExtractor( + /* firstStreamSampleTimestampUs= */ 0, + /* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING); + } + + private void assertFirstSampleAfterSeekContainTargetSeekTime( + FakeTrackOutput trackOutput, long seekTimeUs, int firstSampleIndexAfterSeek) { + long outputSampleTimeUs = trackOutput.getSampleTimeUs(firstSampleIndexAfterSeek); + int expectedSampleIndex = + findOutputSampleInExpectedOutput(trackOutput.getSampleData(firstSampleIndexAfterSeek)); + // Assert that after seeking, the first sample written to output exists in the sample list + assertThat(expectedSampleIndex).isNotEqualTo(-1); + // Assert that the timestamp output for first sample after seek is near the seek point. + // For ADTS seeking, unfortunately we can't guarantee exact sample seeking, since most ADTS + // stream use VBR. + assertThat(Math.abs(outputSampleTimeUs - seekTimeUs)).isLessThan(DELTA_TIMESTAMP_THRESHOLD_US); + assertThat( + Math.abs(outputSampleTimeUs - expectedTrackOutput.getSampleTimeUs(expectedSampleIndex))) + .isLessThan(DELTA_TIMESTAMP_THRESHOLD_US); + trackOutput.assertSample( + firstSampleIndexAfterSeek, + expectedTrackOutput.getSampleData(expectedSampleIndex), + outputSampleTimeUs, + expectedTrackOutput.getSampleFlags(expectedSampleIndex), + expectedTrackOutput.getSampleCryptoData(expectedSampleIndex)); + } + + private int findOutputSampleInExpectedOutput(byte[] sampleData) { + for (int i = 0; i < expectedTrackOutput.getSampleCount(); i++) { + byte[] currentSampleData = expectedTrackOutput.getSampleData(i); + if (Arrays.equals(currentSampleData, sampleData)) { + return i; + } + } + return -1; + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java index 048a23cd67..fe2046cbe4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java @@ -37,4 +37,18 @@ public final class AdtsExtractorTest { }, "ts/sample.adts"); } + + @Test + public void testSample_withSeeking() throws Exception { + ExtractorAsserts.assertBehavior( + new ExtractorFactory() { + @Override + public Extractor create() { + return new AdtsExtractor( + /* firstStreamSampleTimestampUs= */ 0, + /* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING); + } + }, + "ts/sample_cbs.adts"); + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java index 1098ba7563..f7cfd6ccaf 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java @@ -93,14 +93,26 @@ public class AdtsReaderTest { data = new ParsableByteArray( TestUtil.joinByteArrays( + ADTS_HEADER, + ADTS_CONTENT, ADTS_HEADER, ADTS_CONTENT, // Adts sample missing the first sync byte + // The Reader should be able to read the next sample. Arrays.copyOfRange(ADTS_HEADER, 1, ADTS_HEADER.length), + ADTS_CONTENT, + ADTS_HEADER, ADTS_CONTENT)); feed(); - assertSampleCounts(0, 1); - adtsOutput.assertSample(0, ADTS_CONTENT, 0, C.BUFFER_FLAG_KEY_FRAME, null); + assertSampleCounts(0, 3); + for (int i = 0; i < 3; i++) { + adtsOutput.assertSample( + /* index= */ i, + /* data= */ ADTS_CONTENT, + /* timeUs= */ ADTS_SAMPLE_DURATION * i, + /* flags= */ C.BUFFER_FLAG_KEY_FRAME, + /* cryptoData= */ null); + } } @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsDurationReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsDurationReaderTest.java new file mode 100644 index 0000000000..418b2726bf --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsDurationReaderTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.ts; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +/** Unit test for {@link PsDurationReader}. */ +@RunWith(RobolectricTestRunner.class) +public final class PsDurationReaderTest { + + private PsDurationReader tsDurationReader; + private PositionHolder seekPositionHolder; + + @Before + public void setUp() { + tsDurationReader = new PsDurationReader(); + seekPositionHolder = new PositionHolder(); + } + + @Test + public void testIsDurationReadPending_returnFalseByDefault() { + assertThat(tsDurationReader.isDurationReadFinished()).isFalse(); + } + + @Test + public void testReadDuration_returnsCorrectDuration() throws IOException, InterruptedException { + FakeExtractorInput input = + new FakeExtractorInput.Builder() + .setData(TestUtil.getByteArray(RuntimeEnvironment.application, "ts/sample.ps")) + .build(); + + int result = Extractor.RESULT_CONTINUE; + while (!tsDurationReader.isDurationReadFinished()) { + result = tsDurationReader.readDuration(input, seekPositionHolder); + if (result == Extractor.RESULT_SEEK) { + input.setPosition((int) seekPositionHolder.position); + } + } + assertThat(result).isNotEqualTo(Extractor.RESULT_END_OF_INPUT); + assertThat(tsDurationReader.getDurationUs()).isEqualTo(766); + } + + @Test + public void testReadDuration_midStream_returnsCorrectDuration() + throws IOException, InterruptedException { + FakeExtractorInput input = + new FakeExtractorInput.Builder() + .setData(TestUtil.getByteArray(RuntimeEnvironment.application, "ts/sample.ps")) + .build(); + + input.setPosition(1234); + int result = Extractor.RESULT_CONTINUE; + while (!tsDurationReader.isDurationReadFinished()) { + result = tsDurationReader.readDuration(input, seekPositionHolder); + if (result == Extractor.RESULT_SEEK) { + input.setPosition((int) seekPositionHolder.position); + } + } + assertThat(result).isNotEqualTo(Extractor.RESULT_END_OF_INPUT); + assertThat(tsDurationReader.getDurationUs()).isEqualTo(766); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorSeekTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorSeekTest.java new file mode 100644 index 0000000000..33be3a26fd --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorSeekTest.java @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.ts; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.DefaultExtractorInput; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.FakeExtractorOutput; +import com.google.android.exoplayer2.testutil.FakeTrackOutput; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DefaultDataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.Arrays; +import java.util.Random; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +/** Seeking tests for {@link PsExtractor}. */ +@RunWith(RobolectricTestRunner.class) +public final class PsExtractorSeekTest { + + private static final String PS_FILE_PATH = "ts/elephants_dream.mpg"; + private static final int DURATION_US = 30436333; + private static final int VIDEO_TRACK_ID = 224; + private static final long DELTA_TIMESTAMP_THRESHOLD_US = 500_000L; + private static final Random random = new Random(1234L); + + private FakeExtractorOutput expectedOutput; + private FakeTrackOutput expectedTrackOutput; + + private DefaultDataSource dataSource; + private PositionHolder positionHolder; + private long totalInputLength; + + @Before + public void setUp() throws IOException, InterruptedException { + expectedOutput = new FakeExtractorOutput(); + positionHolder = new PositionHolder(); + extractAllSamplesFromFileToExpectedOutput(RuntimeEnvironment.application, PS_FILE_PATH); + expectedTrackOutput = expectedOutput.trackOutputs.get(VIDEO_TRACK_ID); + + dataSource = + new DefaultDataSourceFactory(RuntimeEnvironment.application, "UserAgent") + .createDataSource(); + totalInputLength = readInputLength(); + } + + @Test + public void testPsExtractorReads_nonSeekTableFile_returnSeekableSeekMap() + throws IOException, InterruptedException { + PsExtractor extractor = new PsExtractor(); + + SeekMap seekMap = extractSeekMapAndTracks(extractor, new FakeExtractorOutput()); + + assertThat(seekMap).isNotNull(); + assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US); + assertThat(seekMap.isSeekable()).isTrue(); + } + + @Test + public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame() + throws IOException, InterruptedException { + PsExtractor extractor = new PsExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = extractSeekMapAndTracks(extractor, extractorOutput); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID); + + long targetSeekTimeUs = 987_000; + int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainsTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void testHandlePendingSeek_handlesSeekToEoF() throws IOException, InterruptedException { + PsExtractor extractor = new PsExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = extractSeekMapAndTracks(extractor, extractorOutput); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID); + + long targetSeekTimeUs = seekMap.getDurationUs(); + + int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput); + // Assert that this seek will return a position at end of stream, without any frame. + assertThat(extractedFrameIndex).isEqualTo(-1); + } + + @Test + public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame() + throws IOException, InterruptedException { + PsExtractor extractor = new PsExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = extractSeekMapAndTracks(extractor, extractorOutput); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID); + + long firstSeekTimeUs = 987_000; + seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput); + + long targetSeekTimeUs = 0; + int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainsTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame() + throws IOException, InterruptedException { + PsExtractor extractor = new PsExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = extractSeekMapAndTracks(extractor, extractorOutput); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID); + + long firstSeekTimeUs = 987_000; + seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput); + + long targetSeekTimeUs = 1_234_000; + int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainsTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame() + throws IOException, InterruptedException { + PsExtractor extractor = new PsExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = extractSeekMapAndTracks(extractor, extractorOutput); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID); + + long numSeek = 100; + for (long i = 0; i < numSeek; i++) { + long targetSeekTimeUs = random.nextInt(DURATION_US + 1); + int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainsTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + } + + @Test + public void testHandlePendingSeek_handlesRandomSeeksAfterReadingFileOnce_extractsCorrectFrame() + throws IOException, InterruptedException { + PsExtractor extractor = new PsExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + readInputFileOnce(extractor, extractorOutput); + SeekMap seekMap = extractorOutput.seekMap; + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID); + + long numSeek = 100; + for (long i = 0; i < numSeek; i++) { + long targetSeekTimeUs = random.nextInt(DURATION_US + 1); + int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainsTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + } + + // Internal methods + + private long readInputLength() throws IOException { + DataSpec dataSpec = + new DataSpec(Uri.parse("asset:///" + PS_FILE_PATH), 0, C.LENGTH_UNSET, null); + long totalInputLength = dataSource.open(dataSpec); + Util.closeQuietly(dataSource); + return totalInputLength; + } + + /** + * Seeks to the given seek time and keeps reading from input until we can extract at least one + * frame from the seek position, or until end-of-input is reached. + * + * @return The index of the first extracted frame written to the given {@code trackOutput} after + * the seek is completed, or -1 if the seek is completed without any extracted frame. + */ + private int seekToTimeUs( + PsExtractor psExtractor, SeekMap seekMap, long seekTimeUs, FakeTrackOutput trackOutput) + throws IOException, InterruptedException { + int numSampleBeforeSeek = trackOutput.getSampleCount(); + SeekMap.SeekPoints seekPoints = seekMap.getSeekPoints(seekTimeUs); + + long initialSeekLoadPosition = seekPoints.first.position; + psExtractor.seek(initialSeekLoadPosition, seekTimeUs); + + positionHolder.position = C.POSITION_UNSET; + ExtractorInput extractorInput = getExtractorInputFromPosition(initialSeekLoadPosition); + int extractorReadResult = Extractor.RESULT_CONTINUE; + while (true) { + try { + // Keep reading until we can read at least one frame after seek + while (extractorReadResult == Extractor.RESULT_CONTINUE + && trackOutput.getSampleCount() == numSampleBeforeSeek) { + extractorReadResult = psExtractor.read(extractorInput, positionHolder); + } + } finally { + Util.closeQuietly(dataSource); + } + + if (extractorReadResult == Extractor.RESULT_SEEK) { + extractorInput = getExtractorInputFromPosition(positionHolder.position); + extractorReadResult = Extractor.RESULT_CONTINUE; + } else if (extractorReadResult == Extractor.RESULT_END_OF_INPUT) { + return -1; + } else if (trackOutput.getSampleCount() > numSampleBeforeSeek) { + // First index after seek = num sample before seek. + return numSampleBeforeSeek; + } + } + } + + private SeekMap extractSeekMapAndTracks(PsExtractor extractor, FakeExtractorOutput output) + throws IOException, InterruptedException { + ExtractorInput input = getExtractorInputFromPosition(0); + extractor.init(output); + int readResult = Extractor.RESULT_CONTINUE; + while (true) { + try { + // Keep reading until we can get the seek map + while (readResult == Extractor.RESULT_CONTINUE + && (output.seekMap == null || !output.tracksEnded)) { + readResult = extractor.read(input, positionHolder); + } + } finally { + Util.closeQuietly(dataSource); + } + + if (readResult == Extractor.RESULT_SEEK) { + input = getExtractorInputFromPosition(positionHolder.position); + readResult = Extractor.RESULT_CONTINUE; + } else if (readResult == Extractor.RESULT_END_OF_INPUT) { + throw new IOException("EOF encountered without seekmap"); + } + if (output.seekMap != null) { + return output.seekMap; + } + } + } + + private void readInputFileOnce(PsExtractor extractor, FakeExtractorOutput extractorOutput) + throws IOException, InterruptedException { + extractor.init(extractorOutput); + int readResult = Extractor.RESULT_CONTINUE; + ExtractorInput input = getExtractorInputFromPosition(0); + while (readResult != Extractor.RESULT_END_OF_INPUT) { + try { + while (readResult == Extractor.RESULT_CONTINUE) { + readResult = extractor.read(input, positionHolder); + } + } finally { + Util.closeQuietly(dataSource); + } + if (readResult == Extractor.RESULT_SEEK) { + input = getExtractorInputFromPosition(positionHolder.position); + readResult = Extractor.RESULT_CONTINUE; + } + } + } + + private void assertFirstFrameAfterSeekContainsTargetSeekTime( + FakeTrackOutput trackOutput, long seekTimeUs, int firstFrameIndexAfterSeek) { + long outputSampleTimeUs = trackOutput.getSampleTimeUs(firstFrameIndexAfterSeek); + int expectedSampleIndex = + findOutputFrameInExpectedOutput(trackOutput.getSampleData(firstFrameIndexAfterSeek)); + // Assert that after seeking, the first sample frame written to output exists in the sample list + assertThat(expectedSampleIndex).isNotEqualTo(C.INDEX_UNSET); + + long sampleTimeUs = expectedTrackOutput.getSampleTimeUs(expectedSampleIndex); + if (sampleTimeUs != 0) { + // Assert that the timestamp output for first sample after seek is near the seek point. + // For Ps seeking, unfortunately we can't guarantee exact frame seeking, since PID timestamp + // is not too reliable. + assertThat(Math.abs(outputSampleTimeUs - seekTimeUs)) + .isLessThan(DELTA_TIMESTAMP_THRESHOLD_US); + } + // Assert that the timestamp output for first sample after seek is near the actual sample + // at seek point. + // Note that the timestamp output for first sample after seek might *NOT* be equal to the + // timestamp of that same sample when reading from the beginning, because if first timestamp + // in the stream was not read before the seek, then the timestamp of the first sample after + // the seek is just approximated from the seek point. + assertThat( + Math.abs(outputSampleTimeUs - expectedTrackOutput.getSampleTimeUs(expectedSampleIndex))) + .isLessThan(DELTA_TIMESTAMP_THRESHOLD_US); + trackOutput.assertSample( + firstFrameIndexAfterSeek, + expectedTrackOutput.getSampleData(expectedSampleIndex), + outputSampleTimeUs, + expectedTrackOutput.getSampleFlags(expectedSampleIndex), + expectedTrackOutput.getSampleCryptoData(expectedSampleIndex)); + } + + private int findOutputFrameInExpectedOutput(byte[] sampleData) { + for (int i = 0; i < expectedTrackOutput.getSampleCount(); i++) { + byte[] currentSampleData = expectedTrackOutput.getSampleData(i); + if (Arrays.equals(currentSampleData, sampleData)) { + return i; + } + } + return C.INDEX_UNSET; + } + + private ExtractorInput getExtractorInputFromPosition(long position) throws IOException { + DataSpec dataSpec = + new DataSpec( + Uri.parse("asset:///" + PS_FILE_PATH), position, C.LENGTH_UNSET, /* key= */ null); + dataSource.open(dataSpec); + return new DefaultExtractorInput(dataSource, position, totalInputLength); + } + + private void extractAllSamplesFromFileToExpectedOutput(Context context, String fileName) + throws IOException, InterruptedException { + byte[] data = TestUtil.getByteArray(context, fileName); + + PsExtractor extractor = new PsExtractor(); + extractor.init(expectedOutput); + FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); + + int readResult = Extractor.RESULT_CONTINUE; + while (readResult != Extractor.RESULT_END_OF_INPUT) { + readResult = extractor.read(input, positionHolder); + if (readResult == Extractor.RESULT_SEEK) { + input.setPosition((int) positionHolder.position); + } + } + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.java new file mode 100644 index 0000000000..e7e100f38c --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.ts; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +/** Unit test for {@link TsDurationReader}. */ +@RunWith(RobolectricTestRunner.class) +public final class TsDurationReaderTest { + + private TsDurationReader tsDurationReader; + private PositionHolder seekPositionHolder; + + @Before + public void setUp() { + tsDurationReader = new TsDurationReader(); + seekPositionHolder = new PositionHolder(); + } + + @Test + public void testIsDurationReadPending_returnFalseByDefault() { + assertThat(tsDurationReader.isDurationReadFinished()).isFalse(); + } + + @Test + public void testReadDuration_returnsCorrectDuration() throws IOException, InterruptedException { + FakeExtractorInput input = + new FakeExtractorInput.Builder() + .setData(TestUtil.getByteArray(RuntimeEnvironment.application, "ts/bbb_2500ms.ts")) + .setSimulateIOErrors(false) + .setSimulateUnknownLength(false) + .setSimulatePartialReads(false) + .build(); + + while (!tsDurationReader.isDurationReadFinished()) { + int result = tsDurationReader.readDuration(input, seekPositionHolder, /* pcrPid= */ 256); + if (result == Extractor.RESULT_END_OF_INPUT) { + break; + } + if (result == Extractor.RESULT_SEEK) { + input.setPosition((int) seekPositionHolder.position); + } + } + assertThat(tsDurationReader.getDurationUs() / 1000).isEqualTo(2500); + } + + @Test + public void testReadDuration_midStream_returnsCorrectDuration() + throws IOException, InterruptedException { + FakeExtractorInput input = + new FakeExtractorInput.Builder() + .setData(TestUtil.getByteArray(RuntimeEnvironment.application, "ts/bbb_2500ms.ts")) + .setSimulateIOErrors(false) + .setSimulateUnknownLength(false) + .setSimulatePartialReads(false) + .build(); + + input.setPosition(1234); + while (!tsDurationReader.isDurationReadFinished()) { + int result = tsDurationReader.readDuration(input, seekPositionHolder, /* pcrPid= */ 256); + if (result == Extractor.RESULT_END_OF_INPUT) { + break; + } + if (result == Extractor.RESULT_SEEK) { + input.setPosition((int) seekPositionHolder.position); + } + } + assertThat(tsDurationReader.getDurationUs() / 1000).isEqualTo(2500); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorSeekTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorSeekTest.java new file mode 100644 index 0000000000..4d421b05a4 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorSeekTest.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.ts; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.testutil.FakeExtractorOutput; +import com.google.android.exoplayer2.testutil.FakeTrackOutput; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.DefaultDataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.Arrays; +import java.util.Random; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +/** Seeking tests for {@link TsExtractor}. */ +@RunWith(RobolectricTestRunner.class) +public final class TsExtractorSeekTest { + + private static final String TEST_FILE = "ts/bbb_2500ms.ts"; + private static final int DURATION_US = 2_500_000; + private static final int AUDIO_TRACK_ID = 257; + private static final long MAXIMUM_TIMESTAMP_DELTA_US = 500_000L; + + private static final Random random = new Random(1234L); + + private FakeTrackOutput expectedTrackOutput; + private DefaultDataSource dataSource; + private PositionHolder positionHolder; + + @Before + public void setUp() throws IOException, InterruptedException { + positionHolder = new PositionHolder(); + expectedTrackOutput = + TestUtil.extractAllSamplesFromFile( + new TsExtractor(), RuntimeEnvironment.application, TEST_FILE) + .trackOutputs + .get(AUDIO_TRACK_ID); + + dataSource = + new DefaultDataSourceFactory(RuntimeEnvironment.application, "UserAgent") + .createDataSource(); + } + + @Test + public void testTsExtractorReads_nonSeekTableFile_returnSeekableSeekMap() + throws IOException, InterruptedException { + Uri fileUri = TestUtil.buildAssetUri(TEST_FILE); + TsExtractor extractor = new TsExtractor(); + + SeekMap seekMap = + TestUtil.extractSeekMap(extractor, new FakeExtractorOutput(), dataSource, fileUri); + + assertThat(seekMap).isNotNull(); + assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US); + assertThat(seekMap.isSeekable()).isTrue(); + } + + @Test + public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame() + throws IOException, InterruptedException { + TsExtractor extractor = new TsExtractor(); + Uri fileUri = TestUtil.buildAssetUri(TEST_FILE); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID); + + long targetSeekTimeUs = 987_000; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void testHandlePendingSeek_handlesSeekToEoF_extractsLastFrame() + throws IOException, InterruptedException { + TsExtractor extractor = new TsExtractor(); + Uri fileUri = TestUtil.buildAssetUri(TEST_FILE); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID); + + long targetSeekTimeUs = seekMap.getDurationUs(); + + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame() + throws IOException, InterruptedException { + TsExtractor extractor = new TsExtractor(); + Uri fileUri = TestUtil.buildAssetUri(TEST_FILE); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID); + + long firstSeekTimeUs = 987_000; + TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri); + + long targetSeekTimeUs = 0; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame() + throws IOException, InterruptedException { + TsExtractor extractor = new TsExtractor(); + Uri fileUri = TestUtil.buildAssetUri(TEST_FILE); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID); + + long firstSeekTimeUs = 987_000; + TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri); + + long targetSeekTimeUs = 1_234_000; + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + @Test + public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame() + throws IOException, InterruptedException { + TsExtractor extractor = new TsExtractor(); + Uri fileUri = TestUtil.buildAssetUri(TEST_FILE); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID); + + long numSeek = 100; + for (long i = 0; i < numSeek; i++) { + long targetSeekTimeUs = random.nextInt(DURATION_US + 1); + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + } + + @Test + public void testHandlePendingSeek_handlesRandomSeeksAfterReadingFileOnce_extractsCorrectFrame() + throws IOException, InterruptedException { + TsExtractor extractor = new TsExtractor(); + Uri fileUri = TestUtil.buildAssetUri(TEST_FILE); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + readInputFileOnce(extractor, extractorOutput, fileUri); + SeekMap seekMap = extractorOutput.seekMap; + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID); + + long numSeek = 100; + for (long i = 0; i < numSeek; i++) { + long targetSeekTimeUs = random.nextInt(DURATION_US + 1); + int extractedFrameIndex = + TestUtil.seekToTimeUs( + extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + } + + // Internal methods + + private void readInputFileOnce( + TsExtractor extractor, FakeExtractorOutput extractorOutput, Uri fileUri) + throws IOException, InterruptedException { + extractor.init(extractorOutput); + int readResult = Extractor.RESULT_CONTINUE; + ExtractorInput input = TestUtil.getExtractorInputFromPosition(dataSource, 0, fileUri); + while (readResult != Extractor.RESULT_END_OF_INPUT) { + try { + while (readResult == Extractor.RESULT_CONTINUE) { + readResult = extractor.read(input, positionHolder); + } + } finally { + Util.closeQuietly(dataSource); + } + if (readResult == Extractor.RESULT_SEEK) { + input = + TestUtil.getExtractorInputFromPosition(dataSource, positionHolder.position, fileUri); + readResult = Extractor.RESULT_CONTINUE; + } + } + } + + private void assertFirstFrameAfterSeekContainTargetSeekTime( + FakeTrackOutput trackOutput, long seekTimeUs, int firstFrameIndexAfterSeek) { + long outputSampleTimeUs = trackOutput.getSampleTimeUs(firstFrameIndexAfterSeek); + int expectedSampleIndex = + findOutputFrameInExpectedOutput(trackOutput.getSampleData(firstFrameIndexAfterSeek)); + // Assert that after seeking, the first sample frame written to output exists in the sample list + assertThat(expectedSampleIndex).isNotEqualTo(-1); + // Assert that the timestamp output for first sample after seek is near the seek point. + // For Ts seeking, unfortunately we can't guarantee exact frame seeking, since PID timestamp is + // not too reliable. + assertThat(Math.abs(outputSampleTimeUs - seekTimeUs)).isLessThan(MAXIMUM_TIMESTAMP_DELTA_US); + // Assert that the timestamp output for first sample after seek is near the actual sample + // at seek point. + // Note that the timestamp output for first sample after seek might *NOT* be equal to the + // timestamp of that same sample when reading from the beginning, because if first timestamp in + // the stream was not read before the seek, then the timestamp of the first sample after the + // seek is just approximated from the seek point. + assertThat( + Math.abs(outputSampleTimeUs - expectedTrackOutput.getSampleTimeUs(expectedSampleIndex))) + .isLessThan(MAXIMUM_TIMESTAMP_DELTA_US); + trackOutput.assertSample( + firstFrameIndexAfterSeek, + expectedTrackOutput.getSampleData(expectedSampleIndex), + outputSampleTimeUs, + expectedTrackOutput.getSampleFlags(expectedSampleIndex), + expectedTrackOutput.getSampleCryptoData(expectedSampleIndex)); + } + + private int findOutputFrameInExpectedOutput(byte[] sampleData) { + for (int i = 0; i < expectedTrackOutput.getSampleCount(); i++) { + byte[] currentSampleData = expectedTrackOutput.getSampleData(i); + if (Arrays.equals(currentSampleData, sampleData)) { + return i; + } + } + return -1; + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index 8394ed81a5..2f3813e9e3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -61,15 +61,21 @@ public final class TsExtractorTest { } @Test - public void testIncompleteSample() throws Exception { + public void testStreamWithJunkData() throws Exception { Random random = new Random(0); byte[] fileData = TestUtil.getByteArray(RuntimeEnvironment.application, "ts/sample.ts"); ByteArrayOutputStream out = new ByteArrayOutputStream(fileData.length * 2); + int bytesLeft = fileData.length; + writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1); out.write(fileData, 0, TS_PACKET_SIZE * 5); - for (int i = TS_PACKET_SIZE * 5; i < fileData.length; i += TS_PACKET_SIZE) { + bytesLeft -= TS_PACKET_SIZE * 5; + + for (int i = TS_PACKET_SIZE * 5; i < fileData.length; i += 5 * TS_PACKET_SIZE) { writeJunkData(out, random.nextInt(TS_PACKET_SIZE)); - out.write(fileData, i, TS_PACKET_SIZE); + int length = Math.min(5 * TS_PACKET_SIZE, bytesLeft); + out.write(fileData, i, length); + bytesLeft -= length; } out.write(TS_SYNC_BYTE); writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1); @@ -105,6 +111,9 @@ public final class TsExtractorTest { int readResult = Extractor.RESULT_CONTINUE; while (readResult != Extractor.RESULT_END_OF_INPUT) { readResult = tsExtractor.read(input, seekPositionHolder); + if (readResult == Extractor.RESULT_SEEK) { + input.setPosition((int) seekPositionHolder.position); + } } CustomEsReader reader = factory.esReader; assertThat(reader.packetsRead).isEqualTo(2); @@ -131,8 +140,11 @@ public final class TsExtractorTest { int readResult = Extractor.RESULT_CONTINUE; while (readResult != Extractor.RESULT_END_OF_INPUT) { readResult = tsExtractor.read(input, seekPositionHolder); + if (readResult == Extractor.RESULT_SEEK) { + input.setPosition((int) seekPositionHolder.position); + } } - assertThat(factory.sdtReader.consumedSdts).isEqualTo(1); + assertThat(factory.sdtReader.consumedSdts).isEqualTo(2); } private static void writeJunkData(ByteArrayOutputStream out, int length) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java index 0b992f0981..3fa491ea50 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java @@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.util.Assertions; import java.nio.charset.Charset; import java.util.Arrays; @@ -38,7 +37,7 @@ public final class Id3DecoderTest { private static final int ID3_TEXT_ENCODING_UTF_8 = 3; @Test - public void testDecodeTxxxFrame() throws MetadataDecoderException { + public void testDecodeTxxxFrame() { byte[] rawId3 = buildSingleFrameTag("TXXX", new byte[] {3, 0, 109, 100, 105, 97, 108, 111, 103, 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50, 55, 54, 54, 52, 95, 115, 116, 97, 114, 116, 0}); Id3Decoder decoder = new Id3Decoder(); @@ -65,7 +64,7 @@ public final class Id3DecoderTest { } @Test - public void testDecodeTextInformationFrame() throws MetadataDecoderException { + public void testDecodeTextInformationFrame() { byte[] rawId3 = buildSingleFrameTag("TIT2", new byte[] {3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0}); Id3Decoder decoder = new Id3Decoder(); @@ -92,7 +91,7 @@ public final class Id3DecoderTest { } @Test - public void testDecodeWxxxFrame() throws MetadataDecoderException { + public void testDecodeWxxxFrame() { byte[] rawId3 = buildSingleFrameTag("WXXX", new byte[] {ID3_TEXT_ENCODING_UTF_8, 116, 101, 115, 116, 0, 104, 116, 116, 112, 115, 58, 47, 47, 116, 101, 115, 116, 46, 99, 111, 109, 47, 97, 98, 99, 63, 100, 101, 102}); @@ -120,7 +119,7 @@ public final class Id3DecoderTest { } @Test - public void testDecodeUrlLinkFrame() throws MetadataDecoderException { + public void testDecodeUrlLinkFrame() { byte[] rawId3 = buildSingleFrameTag("WCOM", new byte[] {104, 116, 116, 112, 115, 58, 47, 47, 116, 101, 115, 116, 46, 99, 111, 109, 47, 97, 98, 99, 63, 100, 101, 102}); Id3Decoder decoder = new Id3Decoder(); @@ -142,7 +141,7 @@ public final class Id3DecoderTest { } @Test - public void testDecodePrivFrame() throws MetadataDecoderException { + public void testDecodePrivFrame() { byte[] rawId3 = buildSingleFrameTag("PRIV", new byte[] {116, 101, 115, 116, 0, 1, 2, 3, 4}); Id3Decoder decoder = new Id3Decoder(); Metadata metadata = decoder.decode(rawId3, rawId3.length); @@ -161,7 +160,7 @@ public final class Id3DecoderTest { } @Test - public void testDecodeApicFrame() throws MetadataDecoderException { + public void testDecodeApicFrame() { byte[] rawId3 = buildSingleFrameTag("APIC", new byte[] {3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0}); @@ -177,7 +176,7 @@ public final class Id3DecoderTest { } @Test - public void testDecodeCommentFrame() throws MetadataDecoderException { + public void testDecodeCommentFrame() { byte[] rawId3 = buildSingleFrameTag("COMM", new byte[] {ID3_TEXT_ENCODING_UTF_8, 101, 110, 103, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 0, 116, 101, 120, 116, 0}); Id3Decoder decoder = new Id3Decoder(); @@ -204,7 +203,7 @@ public final class Id3DecoderTest { } @Test - public void testDecodeMultiFrames() throws MetadataDecoderException { + public void testDecodeMultiFrames() { byte[] rawId3 = buildMultiFramesTag( new FrameSpec( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java index 2afe80bb0a..d8a4e97791 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java @@ -19,7 +19,6 @@ import static com.google.android.exoplayer2.C.TIME_UNSET; import static com.google.common.truth.Truth.assertThat; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import com.google.android.exoplayer2.util.TimestampAdjuster; import java.nio.ByteBuffer; @@ -45,7 +44,7 @@ public final class SpliceInfoDecoderTest { } @Test - public void testWrappedAroundTimeSignalCommand() throws MetadataDecoderException { + public void testWrappedAroundTimeSignalCommand() { byte[] rawTimeSignalSection = new byte[] { 0, // table_id. (byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4). @@ -72,7 +71,7 @@ public final class SpliceInfoDecoderTest { } @Test - public void test2SpliceInsertCommands() throws MetadataDecoderException { + public void test2SpliceInsertCommands() { byte[] rawSpliceInsertCommand1 = new byte[] { 0, // table_id. (byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4). @@ -165,8 +164,7 @@ public final class SpliceInfoDecoderTest { assertThat(command.availsExpected).isEqualTo(2); } - private Metadata feedInputBuffer(byte[] data, long timeUs, long subsampleOffset) - throws MetadataDecoderException{ + private Metadata feedInputBuffer(byte[] data, long timeUs, long subsampleOffset) { inputBuffer.clear(); inputBuffer.data = ByteBuffer.allocate(data.length).put(data); inputBuffer.timeUs = timeUs; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java index e821bc34a0..634d541d39 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java @@ -33,9 +33,7 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -/** - * Unit tests for {@link ProgressiveDownloadAction}. - */ +/** Unit tests for {@link ActionFile}. */ @RunWith(RobolectricTestRunner.class) public class ActionFileTest { @@ -258,7 +256,7 @@ public class ActionFileTest { } @Override - protected Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { + public Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { return null; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index 0d0bf73d04..4a1876f69c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -510,7 +510,7 @@ public class DownloadManagerTest { } @Override - protected Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { + public Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { return downloader; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java index bc3732e3d3..df5e7dd044 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java @@ -32,9 +32,7 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -/** - * Unit tests for {@link ProgressiveDownloadAction}. - */ +/** Unit tests for {@link ProgressiveDownloadAction}. */ @RunWith(RobolectricTestRunner.class) public class ProgressiveDownloadActionTest { @@ -49,112 +47,109 @@ public class ProgressiveDownloadActionTest { @Test public void testDownloadActionIsNotRemoveAction() throws Exception { - ProgressiveDownloadAction action = new ProgressiveDownloadAction(uri1, false, null, null); + DownloadAction action = createDownloadAction(uri1, null); assertThat(action.isRemoveAction).isFalse(); } @Test public void testRemoveActionisRemoveAction() throws Exception { - ProgressiveDownloadAction action2 = new ProgressiveDownloadAction(uri1, true, null, null); + DownloadAction action2 = createRemoveAction(uri1, null); assertThat(action2.isRemoveAction).isTrue(); } @Test public void testCreateDownloader() throws Exception { MockitoAnnotations.initMocks(this); - ProgressiveDownloadAction action = new ProgressiveDownloadAction(uri1, false, null, null); - DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper( - Mockito.mock(Cache.class), DummyDataSource.FACTORY); + DownloadAction action = createDownloadAction(uri1, null); + DownloaderConstructorHelper constructorHelper = + new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); assertThat(action.createDownloader(constructorHelper)).isNotNull(); } @Test public void testSameUriCacheKeyDifferentAction_IsSameMedia() throws Exception { - ProgressiveDownloadAction action1 = new ProgressiveDownloadAction(uri1, true, null, null); - ProgressiveDownloadAction action2 = new ProgressiveDownloadAction(uri1, false, null, null); + DownloadAction action1 = createRemoveAction(uri1, null); + DownloadAction action2 = createDownloadAction(uri1, null); assertSameMedia(action1, action2); } @Test public void testNullCacheKeyDifferentUriAction_IsNotSameMedia() throws Exception { - ProgressiveDownloadAction action3 = new ProgressiveDownloadAction(uri2, true, null, null); - ProgressiveDownloadAction action4 = new ProgressiveDownloadAction(uri1, false, null, null); + DownloadAction action3 = createRemoveAction(uri2, null); + DownloadAction action4 = createDownloadAction(uri1, null); assertNotSameMedia(action3, action4); } @Test public void testSameCacheKeyDifferentUriAction_IsSameMedia() throws Exception { - ProgressiveDownloadAction action5 = new ProgressiveDownloadAction(uri2, true, null, "key"); - ProgressiveDownloadAction action6 = new ProgressiveDownloadAction(uri1, false, null, "key"); + DownloadAction action5 = createRemoveAction(uri2, "key"); + DownloadAction action6 = createDownloadAction(uri1, "key"); assertSameMedia(action5, action6); } @Test public void testSameUriDifferentCacheKeyAction_IsNotSameMedia() throws Exception { - ProgressiveDownloadAction action7 = new ProgressiveDownloadAction(uri1, true, null, "key"); - ProgressiveDownloadAction action8 = new ProgressiveDownloadAction(uri1, false, null, "key2"); + DownloadAction action7 = createRemoveAction(uri1, "key"); + DownloadAction action8 = createDownloadAction(uri1, "key2"); assertNotSameMedia(action7, action8); } @Test public void testSameUriNullCacheKeyAction_IsNotSameMedia() throws Exception { - ProgressiveDownloadAction action1 = new ProgressiveDownloadAction(uri1, true, null, "key"); - ProgressiveDownloadAction action2 = new ProgressiveDownloadAction(uri1, false, null, null); + DownloadAction action1 = createRemoveAction(uri1, "key"); + DownloadAction action2 = createDownloadAction(uri1, null); assertNotSameMedia(action1, action2); } @Test public void testEquals() throws Exception { - ProgressiveDownloadAction action1 = new ProgressiveDownloadAction(uri1, true, null, null); + DownloadAction action1 = createRemoveAction(uri1, null); assertThat(action1.equals(action1)).isTrue(); - ProgressiveDownloadAction action2 = new ProgressiveDownloadAction(uri1, true, null, null); - ProgressiveDownloadAction action3 = new ProgressiveDownloadAction(uri1, true, null, null); + DownloadAction action2 = createRemoveAction(uri1, null); + DownloadAction action3 = createRemoveAction(uri1, null); assertThat(action2.equals(action3)).isTrue(); - ProgressiveDownloadAction action4 = new ProgressiveDownloadAction(uri1, true, null, null); - ProgressiveDownloadAction action5 = new ProgressiveDownloadAction(uri1, false, null, null); + DownloadAction action4 = createRemoveAction(uri1, null); + DownloadAction action5 = createDownloadAction(uri1, null); assertThat(action4.equals(action5)).isFalse(); - ProgressiveDownloadAction action6 = new ProgressiveDownloadAction(uri1, true, null, null); - ProgressiveDownloadAction action7 = new ProgressiveDownloadAction(uri1, true, null, "key"); + DownloadAction action6 = createRemoveAction(uri1, null); + DownloadAction action7 = createRemoveAction(uri1, "key"); assertThat(action6.equals(action7)).isFalse(); - ProgressiveDownloadAction action8 = new ProgressiveDownloadAction(uri1, true, null, "key2"); - ProgressiveDownloadAction action9 = new ProgressiveDownloadAction(uri1, true, null, "key"); + DownloadAction action8 = createRemoveAction(uri1, "key2"); + DownloadAction action9 = createRemoveAction(uri1, "key"); assertThat(action8.equals(action9)).isFalse(); - ProgressiveDownloadAction action10 = new ProgressiveDownloadAction(uri1, true, null, null); - ProgressiveDownloadAction action11 = new ProgressiveDownloadAction(uri2, true, null, null); + DownloadAction action10 = createRemoveAction(uri1, null); + DownloadAction action11 = createRemoveAction(uri2, null); assertThat(action10.equals(action11)).isFalse(); } @Test public void testSerializerGetType() throws Exception { - ProgressiveDownloadAction action = new ProgressiveDownloadAction(uri1, false, null, null); + DownloadAction action = createDownloadAction(uri1, null); assertThat(action.type).isNotNull(); } @Test public void testSerializerWriteRead() throws Exception { - doTestSerializationRoundTrip(new ProgressiveDownloadAction(uri1, false, null, null)); - doTestSerializationRoundTrip(new ProgressiveDownloadAction(uri2, true, null, "key")); + doTestSerializationRoundTrip(createDownloadAction(uri1, null)); + doTestSerializationRoundTrip(createRemoveAction(uri2, "key")); } - private void assertSameMedia( - ProgressiveDownloadAction action1, ProgressiveDownloadAction action2) { + private void assertSameMedia(DownloadAction action1, DownloadAction action2) { assertThat(action1.isSameMedia(action2)).isTrue(); assertThat(action2.isSameMedia(action1)).isTrue(); } - private void assertNotSameMedia( - ProgressiveDownloadAction action1, ProgressiveDownloadAction action2) { + private void assertNotSameMedia(DownloadAction action1, DownloadAction action2) { assertThat(action1.isSameMedia(action2)).isFalse(); assertThat(action2.isSameMedia(action1)).isFalse(); } - private static void doTestSerializationRoundTrip(ProgressiveDownloadAction action) - throws IOException { + private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(out); DownloadAction.serializeToStream(action, output); @@ -168,4 +163,11 @@ public class ProgressiveDownloadActionTest { assertThat(action2).isEqualTo(action); } + private static DownloadAction createDownloadAction(Uri uri1, String customCacheKey) { + return ProgressiveDownloadAction.createDownloadAction(uri1, null, customCacheKey); + } + + private static DownloadAction createRemoveAction(Uri uri1, String customCacheKey) { + return ProgressiveDownloadAction.createRemoveAction(uri1, null, customCacheKey); + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index e853529ae6..0209ff86a2 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -37,6 +37,7 @@ import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; import java.io.IOException; import org.junit.Before; import org.junit.Test; @@ -478,7 +479,8 @@ public final class ClippingMediaSourceTest { MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator, - EventDispatcher eventDispatcher) { + EventDispatcher eventDispatcher, + @Nullable TransferListener transferListener) { eventDispatcher.downstreamFormatChanged( new MediaLoadData( C.DATA_TYPE_MEDIA, @@ -488,7 +490,8 @@ public final class ClippingMediaSourceTest { /* trackSelectionData= */ null, C.usToMs(eventStartUs), C.usToMs(eventEndUs))); - return super.createFakeMediaPeriod(id, trackGroupArray, allocator, eventDispatcher); + return super.createFakeMediaPeriod( + id, trackGroupArray, allocator, eventDispatcher, transferListener); } }; final ClippingMediaSource clippingMediaSource = diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 5231fc22ed..f9c327ed2b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -23,8 +23,8 @@ import android.os.ConditionVariable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; @@ -875,21 +875,20 @@ public final class ConcatenatingMediaSourceTest { @Test public void testReleaseAndReprepareSource() throws IOException { - Period period = new Period(); FakeMediaSource[] fakeMediaSources = createMediaSources(/* count= */ 2); mediaSource.addMediaSource(fakeMediaSources[0]); // Child source with 1 period. mediaSource.addMediaSource(fakeMediaSources[1]); // Child source with 2 periods. Timeline timeline = testRunner.prepareSource(); - Object periodId0 = timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).uid; - Object periodId1 = timeline.getPeriod(/* periodIndex= */ 1, period, /* setIds= */ true).uid; - Object periodId2 = timeline.getPeriod(/* periodIndex= */ 2, period, /* setIds= */ true).uid; + Object periodId0 = timeline.getUidOfPeriod(/* periodIndex= */ 0); + Object periodId1 = timeline.getUidOfPeriod(/* periodIndex= */ 1); + Object periodId2 = timeline.getUidOfPeriod(/* periodIndex= */ 2); testRunner.releaseSource(); mediaSource.moveMediaSource(/* currentIndex= */ 1, /* newIndex= */ 0); timeline = testRunner.prepareSource(); - Object newPeriodId0 = timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).uid; - Object newPeriodId1 = timeline.getPeriod(/* periodIndex= */ 1, period, /* setIds= */ true).uid; - Object newPeriodId2 = timeline.getPeriod(/* periodIndex= */ 2, period, /* setIds= */ true).uid; + Object newPeriodId0 = timeline.getUidOfPeriod(/* periodIndex= */ 0); + Object newPeriodId1 = timeline.getUidOfPeriod(/* periodIndex= */ 1); + Object newPeriodId2 = timeline.getUidOfPeriod(/* periodIndex= */ 2); assertThat(newPeriodId0).isEqualTo(periodId1); assertThat(newPeriodId1).isEqualTo(periodId2); assertThat(newPeriodId2).isEqualTo(periodId0); @@ -913,6 +912,58 @@ public final class ConcatenatingMediaSourceTest { new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); } + @Test + public void testChildSourceIsNotPreparedWithLazyPreparation() throws IOException { + FakeMediaSource[] childSources = createMediaSources(/* count= */ 2); + mediaSource = + new ConcatenatingMediaSource( + /* isAtomic= */ false, + /* useLazyPreparation= */ true, + new DefaultShuffleOrder(0), + childSources); + testRunner = new MediaSourceTestRunner(mediaSource, /* allocator= */ null); + testRunner.prepareSource(); + + assertThat(childSources[0].isPrepared()).isFalse(); + assertThat(childSources[1].isPrepared()).isFalse(); + } + + @Test + public void testChildSourceIsPreparedWithLazyPreparationAfterPeriodCreation() throws IOException { + FakeMediaSource[] childSources = createMediaSources(/* count= */ 2); + mediaSource = + new ConcatenatingMediaSource( + /* isAtomic= */ false, + /* useLazyPreparation= */ true, + new DefaultShuffleOrder(0), + childSources); + testRunner = new MediaSourceTestRunner(mediaSource, /* allocator= */ null); + testRunner.prepareSource(); + testRunner.createPeriod(new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + + assertThat(childSources[0].isPrepared()).isTrue(); + assertThat(childSources[1].isPrepared()).isFalse(); + } + + @Test + public void testChildSourceWithLazyPreparationOnlyPreparesSourceOnce() throws IOException { + FakeMediaSource[] childSources = createMediaSources(/* count= */ 2); + mediaSource = + new ConcatenatingMediaSource( + /* isAtomic= */ false, + /* useLazyPreparation= */ true, + new DefaultShuffleOrder(0), + childSources); + testRunner = new MediaSourceTestRunner(mediaSource, /* allocator= */ null); + testRunner.prepareSource(); + + // The lazy preparation must only be triggered once, even if we create multiple periods from + // the media source. FakeMediaSource.prepareSource asserts that it's not called twice, so + // creating two periods shouldn't throw. + testRunner.createPeriod(new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + testRunner.createPeriod(new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + } + private void assertCompletedAllMediaPeriodLoads(Timeline timeline) { Timeline.Period period = new Timeline.Period(); Timeline.Window window = new Timeline.Window(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index d639bc168a..9b7455ee37 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -48,7 +48,7 @@ public class LoopingMediaSourceTest { } @Test - public void testSingleLoop() throws IOException { + public void testSingleLoopTimeline() throws IOException { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1); TimelineAsserts.assertWindowTags(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); @@ -67,7 +67,7 @@ public class LoopingMediaSourceTest { } @Test - public void testMultiLoop() throws IOException { + public void testMultiLoopTimeline() throws IOException { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3); TimelineAsserts.assertWindowTags(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1, 1); @@ -88,7 +88,7 @@ public class LoopingMediaSourceTest { } @Test - public void testInfiniteLoop() throws IOException { + public void testInfiniteLoopTimeline() throws IOException { Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE); TimelineAsserts.assertWindowTags(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); @@ -117,6 +117,21 @@ public class LoopingMediaSourceTest { TimelineAsserts.assertEmpty(timeline); } + @Test + public void testSingleLoopPeriodCreation() throws Exception { + testMediaPeriodCreation(multiWindowTimeline, /* loopCount= */ 1); + } + + @Test + public void testMultiLoopPeriodCreation() throws Exception { + testMediaPeriodCreation(multiWindowTimeline, /* loopCount= */ 3); + } + + @Test + public void testInfiniteLoopPeriodCreation() throws Exception { + testMediaPeriodCreation(multiWindowTimeline, /* loopCount= */ Integer.MAX_VALUE); + } + /** * Wraps the specified timeline in a {@link LoopingMediaSource} and returns the looping timeline. */ @@ -133,4 +148,21 @@ public class LoopingMediaSourceTest { testRunner.release(); } } + + /** + * Wraps the specified timeline in a {@link LoopingMediaSource} and asserts that all periods of + * the looping timeline can be created and prepared. + */ + private static void testMediaPeriodCreation(Timeline timeline, int loopCount) throws Exception { + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); + LoopingMediaSource mediaSource = new LoopingMediaSource(fakeMediaSource, loopCount); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + testRunner.prepareSource(); + testRunner.assertPrepareAndReleaseAllPeriods(); + testRunner.releaseSource(); + } finally { + testRunner.release(); + } + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java index 839492f196..e74347d2f4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java @@ -65,6 +65,27 @@ public class MergingMediaSourceTest { } } + @Test + public void testMergingMediaSourcePeriodCreation() throws Exception { + FakeMediaSource[] mediaSources = new FakeMediaSource[2]; + for (int i = 0; i < mediaSources.length; i++) { + mediaSources[i] = + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2), /* manifest= */ null); + } + MergingMediaSource mediaSource = new MergingMediaSource(mediaSources); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + testRunner.prepareSource(); + testRunner.assertPrepareAndReleaseAllPeriods(); + for (FakeMediaSource element : mediaSources) { + assertThat(element.getCreatedMediaPeriods()).isNotEmpty(); + } + testRunner.releaseSource(); + } finally { + testRunner.release(); + } + } + /** * Wraps the specified timelines in a {@link MergingMediaSource}, prepares it and checks that it * forwards the first of the wrapped timelines. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java index a8cc04473d..da03df9b8a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java @@ -89,6 +89,19 @@ public final class AdPlaybackStateTest { assertThat(state.adGroups[0].states[2]).isEqualTo(AdPlaybackState.AD_STATE_AVAILABLE); } + @Test + public void testGetFirstAdIndexToPlaySkipsSkippedAd() { + state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3); + state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI); + state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI); + + state = state.withSkippedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + + assertThat(state.adGroups[0].getFirstAdIndexToPlay()).isEqualTo(1); + assertThat(state.adGroups[0].states[1]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE); + assertThat(state.adGroups[0].states[2]).isEqualTo(AdPlaybackState.AD_STATE_AVAILABLE); + } + @Test public void testGetFirstAdIndexToPlaySkipsErrorAds() { state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java index c0fa52f74b..155b8f5993 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java @@ -55,7 +55,7 @@ public final class Tx3gDecoderTest { @Test public void testDecodeNoSubtitle() throws IOException, SubtitleDecoderException { - Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); + Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, NO_SUBTITLE); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getCues(0)).isEmpty(); @@ -63,7 +63,7 @@ public final class Tx3gDecoderTest { @Test public void testDecodeJustText() throws IOException, SubtitleDecoderException { - Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); + Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, SAMPLE_JUST_TEXT); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); @@ -74,7 +74,7 @@ public final class Tx3gDecoderTest { @Test public void testDecodeWithStyl() throws IOException, SubtitleDecoderException { - Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); + Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, SAMPLE_WITH_STYL); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); @@ -90,7 +90,7 @@ public final class Tx3gDecoderTest { @Test public void testDecodeWithStylAllDefaults() throws IOException, SubtitleDecoderException { - Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); + Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, SAMPLE_WITH_STYL_ALL_DEFAULTS); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); @@ -102,7 +102,7 @@ public final class Tx3gDecoderTest { @Test public void testDecodeUtf16BeNoStyl() throws IOException, SubtitleDecoderException { - Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); + Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, SAMPLE_UTF16_BE_NO_STYL); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); @@ -113,7 +113,7 @@ public final class Tx3gDecoderTest { @Test public void testDecodeUtf16LeNoStyl() throws IOException, SubtitleDecoderException { - Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); + Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, SAMPLE_UTF16_LE_NO_STYL); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); @@ -124,7 +124,7 @@ public final class Tx3gDecoderTest { @Test public void testDecodeWithMultipleStyl() throws IOException, SubtitleDecoderException { - Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); + Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, SAMPLE_WITH_MULTIPLE_STYL); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); @@ -142,7 +142,7 @@ public final class Tx3gDecoderTest { @Test public void testDecodeWithOtherExtension() throws IOException, SubtitleDecoderException { - Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); + Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, SAMPLE_WITH_OTHER_EXTENSION); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java index b89eb47618..9e42f0c049 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java @@ -246,7 +246,7 @@ public final class WebvttCueParserTest { private static Spanned parseCueText(String string) { WebvttCue.Builder builder = new WebvttCue.Builder(); - WebvttCueParser.parseCueText(null, string, builder, Collections.emptyList()); + WebvttCueParser.parseCueText(null, string, builder, Collections.emptyList()); return (Spanned) builder.build().text; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java index 3074f28b64..af165ffe9b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java @@ -38,8 +38,7 @@ public class WebvttSubtitleTest { private static final String FIRST_AND_SECOND_SUBTITLE_STRING = FIRST_SUBTITLE_STRING + "\n" + SECOND_SUBTITLE_STRING; - private static final WebvttSubtitle emptySubtitle = new WebvttSubtitle( - Collections.emptyList()); + private static final WebvttSubtitle emptySubtitle = new WebvttSubtitle(Collections.emptyList()); private static final WebvttSubtitle simpleSubtitle; static { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java index 4026bc0c37..730572bbd8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java @@ -16,6 +16,10 @@ package com.google.android.exoplayer2.trackselection; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -54,6 +58,23 @@ public final class AdaptiveTrackSelectionTest { fakeClock = new FakeClock(0); } + @Test + public void testFactoryUsesInitiallyProvidedBandwidthMeter() { + BandwidthMeter initialBandwidthMeter = mock(BandwidthMeter.class); + BandwidthMeter injectedBandwidthMeter = mock(BandwidthMeter.class); + Format format = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + AdaptiveTrackSelection adaptiveTrackSelection = + new AdaptiveTrackSelection.Factory(initialBandwidthMeter) + .createTrackSelection(new TrackGroup(format), injectedBandwidthMeter, /* tracks= */ 0); + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 0, + /* availableDurationUs= */ C.TIME_UNSET); + + verify(initialBandwidthMeter, atLeastOnce()).getBitrateEstimate(); + verifyZeroInteractions(injectedBandwidthMeter); + } + @Test public void testSelectInitialIndexUseMaxInitialBitrateIfNoBandwidthEstimate() { Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); @@ -391,11 +412,6 @@ public final class AdaptiveTrackSelectionTest { // Do nothing. } - @Override - public boolean isLoadCanceled() { - return false; - } - @Override public void load() throws IOException, InterruptedException { // Do nothing. @@ -405,10 +421,5 @@ public final class AdaptiveTrackSelectionTest { public boolean isLoadCompleted() { return true; } - - @Override - public long bytesLoaded() { - return 0; - } } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index 2ba63d6773..13314dccf0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -19,9 +19,13 @@ import static com.google.android.exoplayer2.RendererCapabilities.FORMAT_EXCEEDS_ import static com.google.android.exoplayer2.RendererCapabilities.FORMAT_HANDLED; import static com.google.android.exoplayer2.RendererConfiguration.DEFAULT; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyVararg; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import android.os.Parcel; @@ -38,6 +42,7 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Paramet import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.ParametersBuilder; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener; +import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.MimeTypes; import java.util.HashMap; import java.util.Map; @@ -71,33 +76,33 @@ public final class DefaultTrackSelectorTest { private static final RendererCapabilities[] RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER = new RendererCapabilities[] {VIDEO_CAPABILITIES, NO_SAMPLE_CAPABILITIES}; - private static final TrackGroup VIDEO_TRACK_GROUP = - new TrackGroup( - Format.createVideoSampleFormat( - "video", - MimeTypes.VIDEO_H264, - null, - Format.NO_VALUE, - Format.NO_VALUE, - 1024, - 768, - Format.NO_VALUE, - null, - null)); - private static final TrackGroup AUDIO_TRACK_GROUP = - new TrackGroup( - Format.createAudioSampleFormat( - "audio", - MimeTypes.AUDIO_AAC, - null, - Format.NO_VALUE, - Format.NO_VALUE, - 2, - 44100, - null, - null, - 0, - null)); + private static final Format VIDEO_FORMAT = + Format.createVideoSampleFormat( + "video", + MimeTypes.VIDEO_H264, + null, + Format.NO_VALUE, + Format.NO_VALUE, + 1024, + 768, + Format.NO_VALUE, + null, + null); + private static final Format AUDIO_FORMAT = + Format.createAudioSampleFormat( + "audio", + MimeTypes.AUDIO_AAC, + null, + Format.NO_VALUE, + Format.NO_VALUE, + 2, + 44100, + null, + null, + 0, + null); + private static final TrackGroup VIDEO_TRACK_GROUP = new TrackGroup(VIDEO_FORMAT); + private static final TrackGroup AUDIO_TRACK_GROUP = new TrackGroup(AUDIO_FORMAT); private static final TrackGroupArray TRACK_GROUPS = new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP); @@ -110,13 +115,16 @@ public final class DefaultTrackSelectorTest { @Mock private InvalidationListener invalidationListener; + @Mock private BandwidthMeter bandwidthMeter; private DefaultTrackSelector trackSelector; @Before public void setUp() { initMocks(this); + when(bandwidthMeter.getBitrateEstimate()).thenReturn(1000000L); trackSelector = new DefaultTrackSelector(); + trackSelector.init(invalidationListener, bandwidthMeter); } /** Tests {@link Parameters} {@link android.os.Parcelable} implementation. */ @@ -183,6 +191,7 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithNullOverride() throws ExoPlaybackException { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + trackSelector.init(invalidationListener, bandwidthMeter); trackSelector.setParameters( trackSelector .buildUponParameters() @@ -197,6 +206,7 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithClearedNullOverride() throws ExoPlaybackException { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + trackSelector.init(invalidationListener, bandwidthMeter); trackSelector.setParameters( trackSelector .buildUponParameters() @@ -212,6 +222,7 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + trackSelector.init(invalidationListener, bandwidthMeter); trackSelector.setParameters( trackSelector .buildUponParameters() @@ -229,6 +240,7 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithDisabledRenderer() throws ExoPlaybackException { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + trackSelector.init(invalidationListener, bandwidthMeter); trackSelector.setParameters(trackSelector.buildUponParameters().setRendererDisabled(1, true)); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); assertTrackSelections(result, new TrackSelection[] {TRACK_SELECTIONS[0], null}); @@ -240,6 +252,7 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithClearedDisabledRenderer() throws ExoPlaybackException { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + trackSelector.init(invalidationListener, bandwidthMeter); trackSelector.setParameters( trackSelector .buildUponParameters() @@ -255,6 +268,7 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithNoSampleRenderer() throws ExoPlaybackException { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + trackSelector.init(invalidationListener, bandwidthMeter); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); assertTrackSelections(result, TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER); @@ -266,6 +280,7 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithDisabledNoSampleRenderer() throws ExoPlaybackException { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + trackSelector.init(invalidationListener, bandwidthMeter); trackSelector.setParameters(trackSelector.buildUponParameters().setRendererDisabled(1, true)); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); @@ -282,7 +297,7 @@ public final class DefaultTrackSelectorTest { @Test public void testSetParameterWithDefaultParametersDoesNotNotifyInvalidationListener() throws Exception { - trackSelector.init(invalidationListener); + trackSelector.init(invalidationListener, /* bandwidthMeter= */ null); verify(invalidationListener, never()).onTrackSelectionsInvalidated(); } @@ -295,7 +310,7 @@ public final class DefaultTrackSelectorTest { public void testSetParameterWithNonDefaultParameterNotifyInvalidationListener() throws Exception { Parameters parameters = new ParametersBuilder().setPreferredAudioLanguage("eng").build(); - trackSelector.init(invalidationListener); + trackSelector.init(invalidationListener, /* bandwidthMeter= */ null); trackSelector.setParameters(parameters); verify(invalidationListener).onTrackSelectionsInvalidated(); @@ -310,7 +325,7 @@ public final class DefaultTrackSelectorTest { public void testSetParameterWithSameParametersDoesNotNotifyInvalidationListenerAgain() throws Exception { ParametersBuilder builder = new ParametersBuilder().setPreferredAudioLanguage("eng"); - trackSelector.init(invalidationListener); + trackSelector.init(invalidationListener, /* bandwidthMeter= */ null); trackSelector.setParameters(builder.build()); trackSelector.setParameters(builder.build()); @@ -352,9 +367,10 @@ public final class DefaultTrackSelectorTest { Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "eng"); - TrackSelectorResult result = trackSelector.selectTracks( - new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, - singleTrackGroup(frAudioFormat, enAudioFormat)); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, + wrapFormats(frAudioFormat, enAudioFormat)); assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(enAudioFormat); } @@ -754,45 +770,12 @@ public final class DefaultTrackSelectorTest { /** Tests text track selection flags. */ @Test public void testsTextTrackSelectionFlags() throws ExoPlaybackException { - Format forcedOnly = - Format.createTextContainerFormat( - "forcedOnly", - null, - MimeTypes.TEXT_VTT, - null, - Format.NO_VALUE, - C.SELECTION_FLAG_FORCED, - "eng"); + Format forcedOnly = buildTextFormat("forcedOnly", "eng", C.SELECTION_FLAG_FORCED); Format forcedDefault = - Format.createTextContainerFormat( - "forcedDefault", - null, - MimeTypes.TEXT_VTT, - null, - Format.NO_VALUE, - C.SELECTION_FLAG_FORCED | C.SELECTION_FLAG_DEFAULT, - "eng"); - Format defaultOnly = - Format.createTextContainerFormat( - "defaultOnly", - null, - MimeTypes.TEXT_VTT, - null, - Format.NO_VALUE, - C.SELECTION_FLAG_DEFAULT, - "eng"); - Format forcedOnlySpanish = - Format.createTextContainerFormat( - "forcedOnlySpanish", - null, - MimeTypes.TEXT_VTT, - null, - Format.NO_VALUE, - C.SELECTION_FLAG_FORCED, - "spa"); - Format noFlag = - Format.createTextContainerFormat( - "noFlag", null, MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "eng"); + buildTextFormat("forcedDefault", "eng", C.SELECTION_FLAG_FORCED | C.SELECTION_FLAG_DEFAULT); + Format defaultOnly = buildTextFormat("defaultOnly", "eng", C.SELECTION_FLAG_DEFAULT); + Format forcedOnlySpanish = buildTextFormat("forcedOnlySpanish", "spa", C.SELECTION_FLAG_FORCED); + Format noFlag = buildTextFormat("noFlag", "eng"); RendererCapabilities[] textRendererCapabilities = new RendererCapabilities[] {ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}; @@ -886,14 +869,10 @@ public final class DefaultTrackSelectorTest { */ @Test public void testSelectUndeterminedTextLanguageAsFallback() throws ExoPlaybackException{ - Format spanish = Format.createTextContainerFormat("spanish", null, - MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "spa"); - Format german = Format.createTextContainerFormat("german", null, - MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "de"); - Format undeterminedUnd = Format.createTextContainerFormat("undeterminedUnd", null, - MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "und"); - Format undeterminedNull = Format.createTextContainerFormat("undeterminedNull", null, - MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, null); + Format spanish = buildTextFormat("spanish", "spa"); + Format german = buildTextFormat("german", "de"); + Format undeterminedUnd = buildTextFormat("undeterminedUnd", "und"); + Format undeterminedNull = buildTextFormat("undeterminedNull", null); RendererCapabilities[] textRendererCapabilites = new RendererCapabilities[] {ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}; @@ -956,6 +935,108 @@ public final class DefaultTrackSelectorTest { assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(lowerBitrateFormat); } + @Test + public void testSelectTracksWithMultipleAudioTracksReturnsAdaptiveTrackSelection() + throws Exception { + TrackSelection adaptiveTrackSelection = mock(TrackSelection.class); + TrackSelection.Factory adaptiveTrackSelectionFactory = mock(TrackSelection.Factory.class); + when(adaptiveTrackSelectionFactory.createTrackSelection(any(), any(), anyVararg())) + .thenReturn(adaptiveTrackSelection); + + trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory); + trackSelector.init(invalidationListener, bandwidthMeter); + + TrackGroupArray trackGroupArray = singleTrackGroup(AUDIO_FORMAT, AUDIO_FORMAT); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroupArray); + + assertThat(result.length).isEqualTo(1); + assertThat(result.selections.get(0)).isEqualTo(adaptiveTrackSelection); + verify(adaptiveTrackSelectionFactory) + .createTrackSelection(trackGroupArray.get(0), bandwidthMeter, 0, 1); + } + + @Test + public void testSelectTracksWithMultipleAudioTracksOverrideReturnsAdaptiveTrackSelection() + throws Exception { + TrackSelection adaptiveTrackSelection = mock(TrackSelection.class); + TrackSelection.Factory adaptiveTrackSelectionFactory = mock(TrackSelection.Factory.class); + when(adaptiveTrackSelectionFactory.createTrackSelection(any(), any(), anyVararg())) + .thenReturn(adaptiveTrackSelection); + + trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory); + trackSelector.init(invalidationListener, bandwidthMeter); + + TrackGroupArray trackGroupArray = singleTrackGroup(AUDIO_FORMAT, AUDIO_FORMAT, AUDIO_FORMAT); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setSelectionOverride( + /* rendererIndex= */ 0, + trackGroupArray, + new SelectionOverride(/* groupIndex= */ 0, /* tracks= */ 1, 2))); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroupArray); + + assertThat(result.length).isEqualTo(1); + assertThat(result.selections.get(0)).isEqualTo(adaptiveTrackSelection); + verify(adaptiveTrackSelectionFactory) + .createTrackSelection(trackGroupArray.get(0), bandwidthMeter, 1, 2); + } + + @Test + public void testSelectTracksWithMultipleVideoTracksReturnsAdaptiveTrackSelection() + throws Exception { + TrackSelection adaptiveTrackSelection = mock(TrackSelection.class); + TrackSelection.Factory adaptiveTrackSelectionFactory = mock(TrackSelection.Factory.class); + when(adaptiveTrackSelectionFactory.createTrackSelection(any(), any(), anyVararg())) + .thenReturn(adaptiveTrackSelection); + + trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory); + trackSelector.init(invalidationListener, bandwidthMeter); + + TrackGroupArray trackGroupArray = singleTrackGroup(VIDEO_FORMAT, VIDEO_FORMAT); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroupArray); + + assertThat(result.length).isEqualTo(1); + assertThat(result.selections.get(0)).isEqualTo(adaptiveTrackSelection); + verify(adaptiveTrackSelectionFactory) + .createTrackSelection(trackGroupArray.get(0), bandwidthMeter, 0, 1); + } + + @Test + public void testSelectTracksWithMultipleVideoTracksOverrideReturnsAdaptiveTrackSelection() + throws Exception { + TrackSelection adaptiveTrackSelection = mock(TrackSelection.class); + TrackSelection.Factory adaptiveTrackSelectionFactory = mock(TrackSelection.Factory.class); + when(adaptiveTrackSelectionFactory.createTrackSelection(any(), any(), anyVararg())) + .thenReturn(adaptiveTrackSelection); + + trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory); + trackSelector.init(invalidationListener, bandwidthMeter); + + TrackGroupArray trackGroupArray = singleTrackGroup(VIDEO_FORMAT, VIDEO_FORMAT, VIDEO_FORMAT); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setSelectionOverride( + /* rendererIndex= */ 0, + trackGroupArray, + new SelectionOverride(/* groupIndex= */ 0, /* tracks= */ 1, 2))); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroupArray); + + assertThat(result.length).isEqualTo(1); + assertThat(result.selections.get(0)).isEqualTo(adaptiveTrackSelection); + verify(adaptiveTrackSelectionFactory) + .createTrackSelection(trackGroupArray.get(0), bandwidthMeter, 1, 2); + } + private static void assertTrackSelections(TrackSelectorResult result, TrackSelection[] expected) { assertThat(result.length).isEqualTo(expected.length); for (int i = 0; i < expected.length; i++) { @@ -975,6 +1056,22 @@ public final class DefaultTrackSelectorTest { return new TrackGroupArray(trackGroups); } + private static Format buildTextFormat(String id, String language) { + return buildTextFormat(id, language, /* selectionFlags= */ 0); + } + + private static Format buildTextFormat(String id, String language, int selectionFlags) { + return Format.createTextContainerFormat( + id, + /* label= */ null, + /* containerMimeType= */ null, + /* sampleMimeType= */ MimeTypes.TEXT_VTT, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + selectionFlags, + language); + } + /** * A {@link RendererCapabilities} that advertises support for all formats of a given type using * a provided support value. For any format that does not have the given track type, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectorTest.java new file mode 100644 index 0000000000..615f680bb5 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectorTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.trackselection; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener; +import com.google.android.exoplayer2.upstream.BandwidthMeter; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.robolectric.RobolectricTestRunner; + +/** Unit test for {@link TrackSelector}. */ +@RunWith(RobolectricTestRunner.class) +public class TrackSelectorTest { + + private TrackSelector trackSelector; + + @Before + public void setUp() { + trackSelector = new TrackSelector() { + @Override + public TrackSelectorResult selectTracks(RendererCapabilities[] rendererCapabilities, + TrackGroupArray trackGroups) throws ExoPlaybackException { + throw new UnsupportedOperationException(); + } + + @Override + public void onSelectionActivated(Object info) {} + }; + } + + @Test + public void getBandwidthMeter_beforeInitialization_throwsException() { + try { + trackSelector.getBandwidthMeter(); + fail(); + } catch (Exception e) { + // Expected. + } + } + + @Test + public void getBandwidthMeter_afterInitialization_returnsProvidedBandwidthMeter() { + InvalidationListener invalidationListener = Mockito.mock(InvalidationListener.class); + BandwidthMeter bandwidthMeter = Mockito.mock(BandwidthMeter.class); + trackSelector.init(invalidationListener, bandwidthMeter); + + assertThat(trackSelector.getBandwidthMeter()).isEqualTo(bandwidthMeter); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java new file mode 100644 index 0000000000..7155c37e29 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.upstream; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test for {@link BaseDataSource}. */ +@RunWith(RobolectricTestRunner.class) +public class BaseDataSourceTest { + + @Test + public void dataTransfer_withLocalSource_isReported() throws IOException { + TestSource testSource = new TestSource(/* isNetwork= */ false); + TestTransferListener transferListener = new TestTransferListener(); + testSource.addTransferListener(transferListener); + + DataSpec dataSpec = new DataSpec(Uri.EMPTY); + testSource.open(dataSpec); + testSource.read(/* buffer= */ null, /* offset= */ 0, /* readLength= */ 100); + testSource.close(); + + assertThat(transferListener.lastTransferInitializingSource).isSameAs(testSource); + assertThat(transferListener.lastTransferStartSource).isSameAs(testSource); + assertThat(transferListener.lastBytesTransferredSource).isSameAs(testSource); + assertThat(transferListener.lastTransferEndSource).isSameAs(testSource); + + assertThat(transferListener.lastTransferInitializingDataSpec).isEqualTo(dataSpec); + assertThat(transferListener.lastTransferStartDataSpec).isEqualTo(dataSpec); + assertThat(transferListener.lastBytesTransferredDataSpec).isEqualTo(dataSpec); + assertThat(transferListener.lastTransferEndDataSpec).isEqualTo(dataSpec); + + assertThat(transferListener.lastTransferInitializingIsNetwork).isEqualTo(false); + assertThat(transferListener.lastTransferStartIsNetwork).isEqualTo(false); + assertThat(transferListener.lastBytesTransferredIsNetwork).isEqualTo(false); + assertThat(transferListener.lastTransferEndIsNetwork).isEqualTo(false); + + assertThat(transferListener.lastBytesTransferred).isEqualTo(100); + } + + @Test + public void dataTransfer_withRemoteSource_isReported() throws IOException { + TestSource testSource = new TestSource(/* isNetwork= */ true); + TestTransferListener transferListener = new TestTransferListener(); + testSource.addTransferListener(transferListener); + + DataSpec dataSpec = new DataSpec(Uri.EMPTY); + testSource.open(dataSpec); + testSource.read(/* buffer= */ null, /* offset= */ 0, /* readLength= */ 100); + testSource.close(); + + assertThat(transferListener.lastTransferInitializingSource).isSameAs(testSource); + assertThat(transferListener.lastTransferStartSource).isSameAs(testSource); + assertThat(transferListener.lastBytesTransferredSource).isSameAs(testSource); + assertThat(transferListener.lastTransferEndSource).isSameAs(testSource); + + assertThat(transferListener.lastTransferInitializingDataSpec).isEqualTo(dataSpec); + assertThat(transferListener.lastTransferStartDataSpec).isEqualTo(dataSpec); + assertThat(transferListener.lastBytesTransferredDataSpec).isEqualTo(dataSpec); + assertThat(transferListener.lastTransferEndDataSpec).isEqualTo(dataSpec); + + assertThat(transferListener.lastTransferInitializingIsNetwork).isEqualTo(true); + assertThat(transferListener.lastTransferStartIsNetwork).isEqualTo(true); + assertThat(transferListener.lastBytesTransferredIsNetwork).isEqualTo(true); + assertThat(transferListener.lastTransferEndIsNetwork).isEqualTo(true); + + assertThat(transferListener.lastBytesTransferred).isEqualTo(100); + } + + private static final class TestSource extends BaseDataSource { + + public TestSource(boolean isNetwork) { + super(isNetwork); + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + transferInitializing(dataSpec); + transferStarted(dataSpec); + return C.LENGTH_UNSET; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + bytesTransferred(readLength); + return readLength; + } + + @Override + public @Nullable Uri getUri() { + throw new UnsupportedOperationException(); + } + + @Override + public void close() throws IOException { + transferEnded(); + } + } + + private static final class TestTransferListener implements TransferListener { + + public Object lastTransferInitializingSource; + public DataSpec lastTransferInitializingDataSpec; + public boolean lastTransferInitializingIsNetwork; + + public Object lastTransferStartSource; + public DataSpec lastTransferStartDataSpec; + public boolean lastTransferStartIsNetwork; + + public Object lastBytesTransferredSource; + public DataSpec lastBytesTransferredDataSpec; + public boolean lastBytesTransferredIsNetwork; + public int lastBytesTransferred; + + public Object lastTransferEndSource; + public DataSpec lastTransferEndDataSpec; + public boolean lastTransferEndIsNetwork; + + @Override + public void onTransferInitializing(DataSource source, DataSpec dataSpec, boolean isNetwork) { + lastTransferInitializingSource = source; + lastTransferInitializingDataSpec = dataSpec; + lastTransferInitializingIsNetwork = isNetwork; + } + + @Override + public void onTransferStart(DataSource source, DataSpec dataSpec, boolean isNetwork) { + lastTransferStartSource = source; + lastTransferStartDataSpec = dataSpec; + lastTransferStartIsNetwork = isNetwork; + } + + @Override + public void onBytesTransferred( + DataSource source, DataSpec dataSpec, boolean isNetwork, int bytesTransferred) { + lastBytesTransferredSource = source; + lastBytesTransferredDataSpec = dataSpec; + lastBytesTransferredIsNetwork = isNetwork; + lastBytesTransferred = bytesTransferred; + } + + @Override + public void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork) { + lastTransferEndSource = source; + lastTransferEndDataSpec = dataSpec; + lastTransferEndIsNetwork = isNetwork; + } + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicyTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicyTest.java new file mode 100644 index 0000000000..e1700e3b20 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicyTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.upstream; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Collections; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit tests for {@link DefaultLoadErrorHandlingPolicy}. */ +@RunWith(RobolectricTestRunner.class) +public final class DefaultLoadErrorHandlingPolicyTest { + + @Test + public void getBlacklistDurationMsFor_blacklist404() { + InvalidResponseCodeException exception = + new InvalidResponseCodeException(404, Collections.emptyMap(), new DataSpec(Uri.EMPTY)); + assertThat(getDefaultPolicyBlacklistOutputFor(exception)) + .isEqualTo(DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_BLACKLIST_MS); + } + + @Test + public void getBlacklistDurationMsFor_blacklist410() { + InvalidResponseCodeException exception = + new InvalidResponseCodeException(410, Collections.emptyMap(), new DataSpec(Uri.EMPTY)); + assertThat(getDefaultPolicyBlacklistOutputFor(exception)) + .isEqualTo(DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_BLACKLIST_MS); + } + + @Test + public void getBlacklistDurationMsFor_dontBlacklistUnexpectedHttpCodes() { + InvalidResponseCodeException exception = + new InvalidResponseCodeException(500, Collections.emptyMap(), new DataSpec(Uri.EMPTY)); + assertThat(getDefaultPolicyBlacklistOutputFor(exception)).isEqualTo(C.TIME_UNSET); + } + + @Test + public void getBlacklistDurationMsFor_dontBlacklistUnexpectedExceptions() { + FileNotFoundException exception = new FileNotFoundException(); + assertThat(getDefaultPolicyBlacklistOutputFor(exception)).isEqualTo(C.TIME_UNSET); + } + + @Test + public void getRetryDelayMsFor_dontRetryParserException() { + assertThat(getDefaultPolicyRetryDelayOutputFor(new ParserException(), 1)) + .isEqualTo(C.TIME_UNSET); + } + + @Test + public void getRetryDelayMsFor_successiveRetryDelays() { + assertThat(getDefaultPolicyRetryDelayOutputFor(new FileNotFoundException(), 3)).isEqualTo(2000); + assertThat(getDefaultPolicyRetryDelayOutputFor(new FileNotFoundException(), 5)).isEqualTo(4000); + assertThat(getDefaultPolicyRetryDelayOutputFor(new FileNotFoundException(), 9)).isEqualTo(5000); + } + + private static long getDefaultPolicyBlacklistOutputFor(IOException exception) { + return new DefaultLoadErrorHandlingPolicy() + .getBlacklistDurationMsFor( + C.DATA_TYPE_MEDIA, /* loadDurationMs= */ 1000, exception, /* errorCount= */ 1); + } + + private static long getDefaultPolicyRetryDelayOutputFor(IOException exception, int errorCount) { + return new DefaultLoadErrorHandlingPolicy() + .getRetryDelayMsFor(C.DATA_TYPE_MEDIA, /* loadDurationMs= */ 1000, exception, errorCount); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index 8dc702d3a3..6b48cffdd5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -46,16 +46,27 @@ public final class CacheDataSourceTest { private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; private static final int MAX_CACHE_FILE_SIZE = 3; + private static final String CACHE_KEY_PREFIX = "myCacheKeyFactoryPrefix"; private Uri testDataUri; - private String testDataKey; + private String fixedCacheKey; + private String expectedCacheKey; private File tempFolder; private SimpleCache cache; + private CacheKeyFactory cacheKeyFactory; @Before public void setUp() throws Exception { testDataUri = Uri.parse("test_data"); - testDataKey = CacheUtil.generateKey(testDataUri); + fixedCacheKey = CacheUtil.generateKey(testDataUri); + expectedCacheKey = fixedCacheKey; + cacheKeyFactory = + new CacheKeyFactory() { + @Override + public String buildCacheKey(DataSpec dataSpec) { + return CACHE_KEY_PREFIX + "." + CacheUtil.generateKey(dataSpec.uri); + } + }; tempFolder = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); } @@ -77,41 +88,203 @@ public final class CacheDataSourceTest { } } - @Test - public void testCacheAndRead() throws Exception { - assertCacheAndRead(false, false); - } - @Test public void testCacheAndReadUnboundedRequest() throws Exception { - assertCacheAndRead(true, false); + assertCacheAndRead(/* unboundedRequest= */ true, /* simulateUnknownLength= */ false); } @Test public void testCacheAndReadUnknownLength() throws Exception { - assertCacheAndRead(false, true); + assertCacheAndRead(/* unboundedRequest= */ false, /* simulateUnknownLength= */ true); } @Test public void testCacheAndReadUnboundedRequestUnknownLength() throws Exception { - assertCacheAndRead(true, true); + assertCacheAndRead(/* unboundedRequest= */ true, /* simulateUnknownLength= */ true); + } + + @Test + public void testCacheAndRead() throws Exception { + assertCacheAndRead(/* unboundedRequest= */ false, /* simulateUnknownLength= */ false); } @Test public void testUnsatisfiableRange() throws Exception { // Bounded request but the content length is unknown. This forces all data to be cached but not // the length - assertCacheAndRead(false, true); + assertCacheAndRead(/* unboundedRequest= */ false, /* simulateUnknownLength= */ true); // Now do an unbounded request. This will read all of the data from cache and then try to read // more from upstream which will cause to a 416 so CDS will store the length. - CacheDataSource cacheDataSource = createCacheDataSource(true, true); - assertReadDataContentLength(cacheDataSource, true, true); + CacheDataSource cacheDataSource = + createCacheDataSource(/* setReadException= */ true, /* simulateUnknownLength= */ true); + assertReadDataContentLength( + cacheDataSource, /* unboundedRequest= */ true, /* unknownLength= */ true); // If the user try to access off range then it should throw an IOException try { - cacheDataSource = createCacheDataSource(false, false); - cacheDataSource.open(new DataSpec(testDataUri, TEST_DATA.length, 5, testDataKey)); + cacheDataSource = + createCacheDataSource(/* setReadException= */ false, /* simulateUnknownLength= */ false); + cacheDataSource.open(new DataSpec(testDataUri, TEST_DATA.length, 5, fixedCacheKey)); + fail(); + } catch (IOException e) { + // success + } + } + + @Test + public void testCacheAndReadUnboundedRequestWithCacheKeyFactoryWithNullDataSpecCacheKey() + throws Exception { + fixedCacheKey = null; + expectedCacheKey = + cacheKeyFactory.buildCacheKey( + new DataSpec(testDataUri, TEST_DATA.length, 5, /* key= */ null)); + + assertCacheAndRead( + /* unboundedRequest= */ true, /* simulateUnknownLength= */ false, cacheKeyFactory); + } + + @Test + public void testCacheAndReadUnknownLengthWithCacheKeyFactoryOverridingWithNullDataSpecCacheKey() + throws Exception { + fixedCacheKey = null; + expectedCacheKey = + cacheKeyFactory.buildCacheKey( + new DataSpec(testDataUri, TEST_DATA.length, 5, /* key= */ null)); + + assertCacheAndRead( + /* unboundedRequest= */ false, /* simulateUnknownLength= */ true, cacheKeyFactory); + } + + @Test + public void + testCacheAndReadUnboundedRequestUnknownLengthWithCacheKeyFactoryWithNullDataSpecCacheKey() + throws Exception { + fixedCacheKey = null; + expectedCacheKey = + cacheKeyFactory.buildCacheKey( + new DataSpec(testDataUri, TEST_DATA.length, 5, /* key= */ null)); + + assertCacheAndRead( + /* unboundedRequest= */ true, /* simulateUnknownLength= */ true, cacheKeyFactory); + } + + @Test + public void testCacheAndReadWithCacheKeyFactoryWithNullDataSpecCacheKey() throws Exception { + fixedCacheKey = null; + expectedCacheKey = + cacheKeyFactory.buildCacheKey( + new DataSpec(testDataUri, TEST_DATA.length, 5, /* key= */ null)); + + assertCacheAndRead( + /* unboundedRequest= */ false, /* simulateUnknownLength= */ false, cacheKeyFactory); + } + + @Test + public void testUnsatisfiableRangeWithCacheKeyFactoryNullDataSpecCacheKey() throws Exception { + fixedCacheKey = null; + expectedCacheKey = + cacheKeyFactory.buildCacheKey( + new DataSpec(testDataUri, TEST_DATA.length, 5, /* key= */ null)); + + // Bounded request but the content length is unknown. This forces all data to be cached but not + // the length + assertCacheAndRead( + /* unboundedRequest= */ false, /* simulateUnknownLength= */ true, cacheKeyFactory); + + // Now do an unbounded request. This will read all of the data from cache and then try to read + // more from upstream which will cause to a 416 so CDS will store the length. + CacheDataSource cacheDataSource = + createCacheDataSource( + /* setReadException= */ true, /* simulateUnknownLength= */ true, cacheKeyFactory); + assertReadDataContentLength( + cacheDataSource, /* unboundedRequest= */ true, /* unknownLength= */ true); + + // If the user try to access off range then it should throw an IOException + try { + cacheDataSource = + createCacheDataSource( + /* setReadException= */ false, /* simulateUnknownLength= */ false, cacheKeyFactory); + cacheDataSource.open(new DataSpec(testDataUri, TEST_DATA.length, 5, fixedCacheKey)); + fail(); + } catch (IOException e) { + // success + } + } + + @Test + public void testCacheAndReadUnboundedRequestWithCacheKeyFactoryOverridingDataSpecCacheKey() + throws Exception { + fixedCacheKey = CacheUtil.generateKey(testDataUri); + expectedCacheKey = + cacheKeyFactory.buildCacheKey( + new DataSpec(testDataUri, TEST_DATA.length, 5, fixedCacheKey)); + + assertCacheAndRead(true, false, cacheKeyFactory); + } + + @Test + public void testCacheAndReadUnknownLengthWithCacheKeyFactoryOverridingDataSpecCacheKey() + throws Exception { + fixedCacheKey = CacheUtil.generateKey(testDataUri); + expectedCacheKey = + cacheKeyFactory.buildCacheKey( + new DataSpec(testDataUri, TEST_DATA.length, 5, fixedCacheKey)); + + assertCacheAndRead(false, true, cacheKeyFactory); + } + + @Test + public void + testCacheAndReadUnboundedRequestUnknownLengthWithCacheKeyFactoryOverridingDataSpecCacheKey() + throws Exception { + fixedCacheKey = CacheUtil.generateKey(testDataUri); + expectedCacheKey = + cacheKeyFactory.buildCacheKey( + new DataSpec(testDataUri, TEST_DATA.length, 5, fixedCacheKey)); + + assertCacheAndRead( + /* unboundedRequest= */ true, /* simulateUnknownLength= */ true, cacheKeyFactory); + } + + @Test + public void testCacheAndReadWithCacheKeyFactoryOverridingDataSpecCacheKey() throws Exception { + fixedCacheKey = CacheUtil.generateKey(testDataUri); + expectedCacheKey = + cacheKeyFactory.buildCacheKey( + new DataSpec(testDataUri, TEST_DATA.length, 5, fixedCacheKey)); + + assertCacheAndRead( + /* unboundedRequest= */ false, /* simulateUnknownLength= */ false, cacheKeyFactory); + } + + @Test + public void testUnsatisfiableRangeWithCacheKeyFactoryOverridingDataSpecCacheKey() + throws Exception { + fixedCacheKey = CacheUtil.generateKey(testDataUri); + expectedCacheKey = + cacheKeyFactory.buildCacheKey( + new DataSpec(testDataUri, TEST_DATA.length, 5, fixedCacheKey)); + + // Bounded request but the content length is unknown. This forces all data to be cached but not + // the length + assertCacheAndRead( + /* unboundedRequest= */ false, /* simulateUnknownLength= */ true, cacheKeyFactory); + + // Now do an unbounded request. This will read all of the data from cache and then try to read + // more from upstream which will cause to a 416 so CDS will store the length. + CacheDataSource cacheDataSource = + createCacheDataSource( + /* setReadException= */ true, /* simulateUnknownLength= */ true, cacheKeyFactory); + assertReadDataContentLength( + cacheDataSource, /* unboundedRequest= */ true, /* unknownLength= */ true); + + // If the user try to access off range then it should throw an IOException + try { + cacheDataSource = + createCacheDataSource( + /* setReadException= */ false, /* simulateUnknownLength= */ false, cacheKeyFactory); + cacheDataSource.open(new DataSpec(testDataUri, TEST_DATA.length, 5, fixedCacheKey)); fail(); } catch (IOException e) { // success @@ -123,7 +296,7 @@ public final class CacheDataSourceTest { // Read partial at EOS but don't cross it so length is unknown CacheDataSource cacheDataSource = createCacheDataSource(false, true); assertReadData(cacheDataSource, true, TEST_DATA.length - 2, 2); - assertThat(cache.getContentLength(testDataKey)).isEqualTo(C.LENGTH_UNSET); + assertThat(cache.getContentLength(expectedCacheKey)).isEqualTo(C.LENGTH_UNSET); // Now do an unbounded request for whole data. This will cause a bounded request from upstream. // End of data from upstream shouldn't be mixed up with EOS and cause length set wrong. @@ -133,7 +306,7 @@ public final class CacheDataSourceTest { // Now the length set correctly do an unbounded request with offset assertThat( cacheDataSource.open( - new DataSpec(testDataUri, TEST_DATA.length - 2, C.LENGTH_UNSET, testDataKey))) + new DataSpec(testDataUri, TEST_DATA.length - 2, C.LENGTH_UNSET, expectedCacheKey))) .isEqualTo(2); // An unbounded request with offset for not cached content @@ -155,12 +328,12 @@ public final class CacheDataSourceTest { CacheDataSource cacheDataSource = new CacheDataSource(cache, upstream, 0); int flags = DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH; - cacheDataSource.open(new DataSpec(testDataUri, 0, C.LENGTH_UNSET, testDataKey, flags)); + cacheDataSource.open(new DataSpec(testDataUri, 0, C.LENGTH_UNSET, expectedCacheKey, flags)); TestUtil.readToEnd(cacheDataSource); cacheDataSource.close(); assertThat(upstream.getAndClearOpenedDataSpecs()).hasLength(1); - assertThat(cache.getContentLength(testDataKey)).isEqualTo(TEST_DATA.length); + assertThat(cache.getContentLength(expectedCacheKey)).isEqualTo(TEST_DATA.length); } @Test @@ -171,7 +344,7 @@ public final class CacheDataSourceTest { new CacheDataSource( cache, upstream, CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS); - cacheDataSource.open(new DataSpec(testDataUri, 0, C.LENGTH_UNSET, testDataKey)); + cacheDataSource.open(new DataSpec(testDataUri, 0, C.LENGTH_UNSET, expectedCacheKey)); TestUtil.readToEnd(cacheDataSource); cacheDataSource.close(); @@ -206,7 +379,7 @@ public final class CacheDataSourceTest { new CacheDataSource(cache, upstream, new FileDataSource(), null, 0, null); // Open source and read some data from upstream as the data hasn't cached yet. - DataSpec dataSpec = new DataSpec(testDataUri, 0, C.LENGTH_UNSET, testDataKey); + DataSpec dataSpec = new DataSpec(testDataUri, 0, C.LENGTH_UNSET, fixedCacheKey); cacheDataSource.open(dataSpec); byte[] buffer = new byte[1024]; cacheDataSource.read(buffer, 0, buffer.length); @@ -245,7 +418,7 @@ public final class CacheDataSourceTest { .appendReadData(1); // Lock the content on the cache. - SimpleCacheSpan cacheSpan = cache.startReadWriteNonBlocking(testDataKey, 0); + SimpleCacheSpan cacheSpan = cache.startReadWriteNonBlocking(expectedCacheKey, 0); assertThat(cacheSpan).isNotNull(); assertThat(cacheSpan.isHoleSpan()).isTrue(); @@ -253,7 +426,7 @@ public final class CacheDataSourceTest { CacheDataSource cacheDataSource = new CacheDataSource(cache, upstream, 0); // Open source and read some data from upstream without writing to cache as the data is locked. - DataSpec dataSpec = new DataSpec(testDataUri, 0, C.LENGTH_UNSET, testDataKey); + DataSpec dataSpec = new DataSpec(testDataUri, 0, C.LENGTH_UNSET, fixedCacheKey); cacheDataSource.open(dataSpec); byte[] buffer = new byte[1024]; cacheDataSource.read(buffer, 0, buffer.length); @@ -286,7 +459,7 @@ public final class CacheDataSourceTest { upstream.getDataSet().newDefaultData().appendReadData(1024).endData(); // Cache the latter half of the data. - DataSpec dataSpec = new DataSpec(testDataUri, 512, C.LENGTH_UNSET, testDataKey); + DataSpec dataSpec = new DataSpec(testDataUri, 512, C.LENGTH_UNSET, fixedCacheKey); CacheUtil.cache(dataSpec, cache, upstream, /* counters= */ null, /* isCanceled= */ null); // Create cache read-only CacheDataSource. @@ -294,12 +467,12 @@ public final class CacheDataSourceTest { new CacheDataSource(cache, upstream, new FileDataSource(), null, 0, null); // Open source and read some data from upstream as the data hasn't cached yet. - dataSpec = new DataSpec(testDataUri, 0, C.LENGTH_UNSET, testDataKey); + dataSpec = new DataSpec(testDataUri, 0, C.LENGTH_UNSET, fixedCacheKey); cacheDataSource.open(dataSpec); TestUtil.readExactly(cacheDataSource, 100); // Delete cached data. - CacheUtil.remove(cache, testDataKey); + CacheUtil.remove(cache, expectedCacheKey); assertCacheEmpty(cache); // Read the rest of the data. @@ -317,21 +490,21 @@ public final class CacheDataSourceTest { // Cache the latter half of the data. int halfDataLength = 512; - DataSpec dataSpec = new DataSpec(testDataUri, halfDataLength, C.LENGTH_UNSET, testDataKey); + DataSpec dataSpec = new DataSpec(testDataUri, halfDataLength, C.LENGTH_UNSET, fixedCacheKey); CacheUtil.cache(dataSpec, cache, upstream, /* counters= */ null, /* isCanceled= */ null); // Create blocking CacheDataSource. CacheDataSource cacheDataSource = new CacheDataSource(cache, upstream, CacheDataSource.FLAG_BLOCK_ON_CACHE); - dataSpec = new DataSpec(testDataUri, 0, C.LENGTH_UNSET, testDataKey); + dataSpec = new DataSpec(testDataUri, 0, C.LENGTH_UNSET, fixedCacheKey); cacheDataSource.open(dataSpec); // Read the first half from upstream as it hasn't cached yet. TestUtil.readExactly(cacheDataSource, halfDataLength); // Delete the cached latter half. - NavigableSet cachedSpans = cache.getCachedSpans(testDataKey); + NavigableSet cachedSpans = cache.getCachedSpans(expectedCacheKey); for (CacheSpan cachedSpan : cachedSpans) { if (cachedSpan.position >= halfDataLength) { try { @@ -355,8 +528,30 @@ public final class CacheDataSourceTest { // Just read from cache cacheDataSource = createCacheDataSource(true, simulateUnknownLength); - assertReadDataContentLength(cacheDataSource, unboundedRequest, - false /*length is already cached*/); + assertReadDataContentLength( + cacheDataSource, + unboundedRequest, + // Length is already cached. + /* unknownLength= */ false); + } + + private void assertCacheAndRead( + boolean unboundedRequest, boolean simulateUnknownLength, CacheKeyFactory cacheKeyFactory) + throws IOException { + // Read all data from upstream and write to cache + CacheDataSource cacheDataSource = + createCacheDataSource( + /* setReadException= */ false, simulateUnknownLength, cacheKeyFactory); + assertReadDataContentLength(cacheDataSource, unboundedRequest, simulateUnknownLength); + + // Just read from cache + cacheDataSource = + createCacheDataSource(/* setReadException= */ true, simulateUnknownLength, cacheKeyFactory); + assertReadDataContentLength( + cacheDataSource, + unboundedRequest, + // Length is already cached. + /* unknownLength= */ false); } /** @@ -368,7 +563,7 @@ public final class CacheDataSourceTest { int length = unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length; assertReadData(cacheDataSource, unknownLength, 0, length); // If !unboundedRequest, CacheDataSource doesn't reach EOS so shouldn't cache content length - assertThat(cache.getContentLength(testDataKey)) + assertThat(cache.getContentLength(expectedCacheKey)) .isEqualTo(!unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length); } @@ -380,7 +575,11 @@ public final class CacheDataSourceTest { } DataSpec dataSpec = new DataSpec( - testDataUri, position, length, testDataKey, DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH); + testDataUri, + position, + length, + fixedCacheKey, + DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH); assertThat(cacheDataSource.open(dataSpec)).isEqualTo(unknownLength ? length : testDataLength); cacheDataSource.close(); @@ -395,6 +594,16 @@ public final class CacheDataSourceTest { CacheDataSource.FLAG_BLOCK_ON_CACHE); } + private CacheDataSource createCacheDataSource( + boolean setReadException, boolean simulateUnknownLength, CacheKeyFactory cacheKeyFactory) { + return createCacheDataSource( + setReadException, + simulateUnknownLength, + CacheDataSource.FLAG_BLOCK_ON_CACHE, + new CacheDataSink(cache, MAX_CACHE_FILE_SIZE), + cacheKeyFactory); + } + private CacheDataSource createCacheDataSource(boolean setReadException, boolean simulateUnknownLength, @CacheDataSource.Flags int flags) { return createCacheDataSource(setReadException, simulateUnknownLength, flags, @@ -404,14 +613,34 @@ public final class CacheDataSourceTest { private CacheDataSource createCacheDataSource(boolean setReadException, boolean simulateUnknownLength, @CacheDataSource.Flags int flags, CacheDataSink cacheWriteDataSink) { + return createCacheDataSource( + setReadException, + simulateUnknownLength, + flags, + cacheWriteDataSink, + /* cacheKeyFactory= */ null); + } + + private CacheDataSource createCacheDataSource( + boolean setReadException, + boolean simulateUnknownLength, + @CacheDataSource.Flags int flags, + CacheDataSink cacheWriteDataSink, + CacheKeyFactory cacheKeyFactory) { FakeDataSource upstream = new FakeDataSource(); FakeData fakeData = upstream.getDataSet().newDefaultData() .setSimulateUnknownLength(simulateUnknownLength).appendReadData(TEST_DATA); if (setReadException) { fakeData.appendReadError(new IOException("Shouldn't read from upstream")); } - return new CacheDataSource(cache, upstream, new FileDataSource(), cacheWriteDataSink, - flags, null); + return new CacheDataSource( + cache, + upstream, + new FileDataSource(), + cacheWriteDataSink, + flags, + /* eventListener= */ null, + cacheKeyFactory); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java index 61c7f2b673..e3917b58d0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java @@ -131,6 +131,22 @@ public final class CacheUtilTest { .isEqualTo(generateKey(testUri)); } + @Test + public void testDefaultCacheKeyFactory_buildCacheKey() throws Exception { + Uri testUri = Uri.parse("test"); + String key = "key"; + // If DataSpec.key is present, returns it + assertThat( + CacheUtil.DEFAULT_CACHE_KEY_FACTORY.buildCacheKey( + new DataSpec(testUri, 0, LENGTH_UNSET, key))) + .isEqualTo(key); + // If not generates a new one using DataSpec.uri + assertThat( + CacheUtil.DEFAULT_CACHE_KEY_FACTORY.buildCacheKey( + new DataSpec(testUri, 0, LENGTH_UNSET, null))) + .isEqualTo(generateKey(testUri)); + } + @Test public void testGetCachedNoData() throws Exception { CachingCounters counters = new CachingCounters(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index cdd5d1a696..baf8aa7c40 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -27,8 +27,10 @@ import static com.google.common.truth.Truth.assertThat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Random; +import java.util.zip.Deflater; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -247,6 +249,23 @@ public class UtilTest { } } + @Test + public void testInflate() { + byte[] testData = TestUtil.buildTestData(/*arbitrary test data size*/ 256 * 1024); + byte[] compressedData = new byte[testData.length * 2]; + Deflater compresser = new Deflater(9); + compresser.setInput(testData); + compresser.finish(); + int compressedDataLength = compresser.deflate(compressedData); + compresser.end(); + + ParsableByteArray input = new ParsableByteArray(compressedData, compressedDataLength); + ParsableByteArray output = new ParsableByteArray(); + assertThat(Util.inflate(input, output, /* inflater= */ null)).isTrue(); + assertThat(output.limit()).isEqualTo(testData.length); + assertThat(Arrays.copyOf(output.data, output.limit())).isEqualTo(testData); + } + private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) { assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName); assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); diff --git a/library/dash/build.gradle b/library/dash/build.gradle index 867b288498..40b014aaf9 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -18,9 +18,15 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion + consumerProguardFiles 'proguard-rules.txt' } buildTypes { diff --git a/library/dash/proguard-rules.txt b/library/dash/proguard-rules.txt new file mode 100644 index 0000000000..f8725fff4d --- /dev/null +++ b/library/dash/proguard-rules.txt @@ -0,0 +1,7 @@ +# Proguard rules specific to the dash module. + +# Constructors accessed via reflection in SegmentDownloadAction +-dontnote com.google.android.exoplayer2.source.dash.offline.DashDownloadAction +-keepclassmembers class com.google.android.exoplayer2.source.dash.offline.DashDownloadAction { + static ** DESERIALIZER; +} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java index 31c32e6100..c6cdc88f2f 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEm import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; +import com.google.android.exoplayer2.upstream.TransferListener; /** * An {@link ChunkSource} for DASH streams. @@ -44,6 +45,8 @@ public interface DashChunkSource extends ChunkSource { * message track. * @param enableCea608Track Whether the chunks generated by the source may output a CEA-608 * track. + * @param transferListener The transfer listener which should be informed of any data transfers. + * May be null if no listener is available. * @return The created {@link DashChunkSource}. */ DashChunkSource createDashChunkSource( @@ -56,7 +59,8 @@ public interface DashChunkSource extends ChunkSource { long elapsedRealtimeOffsetMs, boolean enableEventMessageTrack, boolean enableCea608Track, - @Nullable PlayerTrackEmsgHandler playerEmsgHandler); + @Nullable PlayerTrackEmsgHandler playerEmsgHandler, + @Nullable TransferListener transferListener); } /** diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index d2982481e0..d52049931f 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.source.dash; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Pair; -import android.util.SparseArray; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -42,7 +42,9 @@ import com.google.android.exoplayer2.source.dash.manifest.Period; import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.lang.annotation.Retention; @@ -60,8 +62,8 @@ import java.util.List; /* package */ final int id; private final DashChunkSource.Factory chunkSourceFactory; - private final int minLoadableRetryCount; - private final EventDispatcher eventDispatcher; + private final @Nullable TransferListener transferListener; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long elapsedRealtimeOffset; private final LoaderErrorThrower manifestLoaderErrorThrower; private final Allocator allocator; @@ -72,7 +74,8 @@ import java.util.List; private final IdentityHashMap, PlayerTrackEmsgHandler> trackEmsgHandlerBySampleStream; - private Callback callback; + private EventDispatcher eventDispatcher; + private @Nullable Callback callback; private ChunkSampleStream[] sampleStreams; private EventSampleStream[] eventSampleStreams; private SequenceableLoader compositeSequenceableLoader; @@ -86,7 +89,8 @@ import java.util.List; DashManifest manifest, int periodIndex, DashChunkSource.Factory chunkSourceFactory, - int minLoadableRetryCount, + @Nullable TransferListener transferListener, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, long elapsedRealtimeOffset, LoaderErrorThrower manifestLoaderErrorThrower, @@ -97,7 +101,8 @@ import java.util.List; this.manifest = manifest; this.periodIndex = periodIndex; this.chunkSourceFactory = chunkSourceFactory; - this.minLoadableRetryCount = minLoadableRetryCount; + this.transferListener = transferListener; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.elapsedRealtimeOffset = elapsedRealtimeOffset; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; @@ -126,6 +131,13 @@ import java.util.List; */ public void updateManifest(DashManifest manifest, int periodIndex) { this.manifest = manifest; + if (this.periodIndex != periodIndex) { + eventDispatcher = + eventDispatcher.withParameters( + /* windowIndex= */ 0, + eventDispatcher.mediaPeriodId.copyWithPeriodIndex(periodIndex), + manifest.getPeriod(periodIndex).startMs); + } this.periodIndex = periodIndex; playerEmsgHandler.updateManifest(manifest); if (sampleStreams != null) { @@ -138,7 +150,10 @@ import java.util.List; for (EventSampleStream eventSampleStream : eventSampleStreams) { for (EventStream eventStream : eventStreams) { if (eventStream.id().equals(eventSampleStream.eventStreamId())) { - eventSampleStream.updateEventStream(eventStream, manifest.dynamic); + int lastPeriodIndex = manifest.getPeriodCount() - 1; + eventSampleStream.updateEventStream( + eventStream, + /* eventStreamAppendable= */ manifest.dynamic && periodIndex == lastPeriodIndex); break; } } @@ -150,6 +165,7 @@ import java.util.List; for (ChunkSampleStream sampleStream : sampleStreams) { sampleStream.release(this); } + callback = null; eventDispatcher.mediaPeriodReleased(); } @@ -184,126 +200,34 @@ import java.util.List; @Override public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - SparseArray> primarySampleStreams = new SparseArray<>(); - List eventSampleStreamList = new ArrayList<>(); + int[] streamIndexToTrackGroupIndex = getStreamIndexToTrackGroupIndex(selections); + releaseDisabledStreams(selections, mayRetainStreamFlags, streams); + releaseOrphanEmbeddedStreams(selections, streams, streamIndexToTrackGroupIndex); + selectNewStreams( + selections, streams, streamResetFlags, positionUs, streamIndexToTrackGroupIndex); - selectPrimarySampleStreams(selections, mayRetainStreamFlags, streams, streamResetFlags, - positionUs, primarySampleStreams); - selectEventSampleStreams(selections, mayRetainStreamFlags, streams, - streamResetFlags, eventSampleStreamList); - selectEmbeddedSampleStreams(selections, mayRetainStreamFlags, streams, streamResetFlags, - positionUs, primarySampleStreams); - - sampleStreams = newSampleStreamArray(primarySampleStreams.size()); - for (int i = 0; i < sampleStreams.length; i++) { - sampleStreams[i] = primarySampleStreams.valueAt(i); + ArrayList> sampleStreamList = new ArrayList<>(); + ArrayList eventSampleStreamList = new ArrayList<>(); + for (SampleStream sampleStream : streams) { + if (sampleStream instanceof ChunkSampleStream) { + @SuppressWarnings("unchecked") + ChunkSampleStream stream = + (ChunkSampleStream) sampleStream; + sampleStreamList.add(stream); + } else if (sampleStream instanceof EventSampleStream) { + eventSampleStreamList.add((EventSampleStream) sampleStream); + } } + sampleStreams = newSampleStreamArray(sampleStreamList.size()); + sampleStreamList.toArray(sampleStreams); eventSampleStreams = new EventSampleStream[eventSampleStreamList.size()]; eventSampleStreamList.toArray(eventSampleStreams); + compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); return positionUs; } - private void selectPrimarySampleStreams( - TrackSelection[] selections, - boolean[] mayRetainStreamFlags, - SampleStream[] streams, - boolean[] streamResetFlags, - long positionUs, - SparseArray> primarySampleStreams) { - for (int i = 0; i < selections.length; i++) { - if (streams[i] instanceof ChunkSampleStream) { - @SuppressWarnings("unchecked") - ChunkSampleStream stream = (ChunkSampleStream) streams[i]; - if (selections[i] == null || !mayRetainStreamFlags[i]) { - stream.release(this); - streams[i] = null; - } else { - int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - primarySampleStreams.put(trackGroupIndex, stream); - } - } - - if (streams[i] == null && selections[i] != null) { - int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; - if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) { - ChunkSampleStream stream = buildSampleStream(trackGroupInfo, - selections[i], positionUs); - primarySampleStreams.put(trackGroupIndex, stream); - streams[i] = stream; - streamResetFlags[i] = true; - } - } - } - } - - private void selectEventSampleStreams(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, - List eventSampleStreamsList) { - for (int i = 0; i < selections.length; i++) { - if (streams[i] instanceof EventSampleStream) { - EventSampleStream stream = (EventSampleStream) streams[i]; - if (selections[i] == null || !mayRetainStreamFlags[i]) { - streams[i] = null; - } else { - eventSampleStreamsList.add(stream); - } - } - - if (streams[i] == null && selections[i] != null) { - int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; - if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_MANIFEST_EVENTS) { - EventStream eventStream = eventStreams.get(trackGroupInfo.eventStreamGroupIndex); - Format format = selections[i].getTrackGroup().getFormat(0); - EventSampleStream stream = new EventSampleStream(eventStream, format, manifest.dynamic); - streams[i] = stream; - streamResetFlags[i] = true; - eventSampleStreamsList.add(stream); - } - } - } - } - - private void selectEmbeddedSampleStreams( - TrackSelection[] selections, - boolean[] mayRetainStreamFlags, - SampleStream[] streams, - boolean[] streamResetFlags, - long positionUs, - SparseArray> primarySampleStreams) { - for (int i = 0; i < selections.length; i++) { - if ((streams[i] instanceof EmbeddedSampleStream || streams[i] instanceof EmptySampleStream) - && (selections[i] == null || !mayRetainStreamFlags[i])) { - // The stream is for an embedded track and is either no longer selected or needs replacing. - releaseIfEmbeddedSampleStream(streams[i]); - streams[i] = null; - } - // We need to consider replacing the stream even if it's non-null because the primary stream - // may have been replaced, selected or deselected. - if (selections[i] != null) { - int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; - if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_EMBEDDED) { - ChunkSampleStream primaryStream = primarySampleStreams.get( - trackGroupInfo.primaryTrackGroupIndex); - SampleStream stream = streams[i]; - boolean mayRetainStream = primaryStream == null ? stream instanceof EmptySampleStream - : (stream instanceof EmbeddedSampleStream - && ((EmbeddedSampleStream) stream).parent == primaryStream); - if (!mayRetainStream) { - releaseIfEmbeddedSampleStream(stream); - streams[i] = primaryStream == null ? new EmptySampleStream() - : primaryStream.selectEmbeddedTrack(positionUs, trackGroupInfo.trackType); - streamResetFlags[i] = true; - } - } - } - } - } - @Override public void discardBuffer(long positionUs, boolean toKeyframe) { for (ChunkSampleStream sampleStream : sampleStreams) { @@ -370,6 +294,124 @@ import java.util.List; // Internal methods. + private int[] getStreamIndexToTrackGroupIndex(TrackSelection[] selections) { + int[] streamIndexToTrackGroupIndex = new int[selections.length]; + for (int i = 0; i < selections.length; i++) { + if (selections[i] != null) { + streamIndexToTrackGroupIndex[i] = trackGroups.indexOf(selections[i].getTrackGroup()); + } else { + streamIndexToTrackGroupIndex[i] = C.INDEX_UNSET; + } + } + return streamIndexToTrackGroupIndex; + } + + private void releaseDisabledStreams( + TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams) { + for (int i = 0; i < selections.length; i++) { + if (selections[i] == null || !mayRetainStreamFlags[i]) { + if (streams[i] instanceof ChunkSampleStream) { + @SuppressWarnings("unchecked") + ChunkSampleStream stream = + (ChunkSampleStream) streams[i]; + stream.release(this); + } else if (streams[i] instanceof EmbeddedSampleStream) { + ((EmbeddedSampleStream) streams[i]).release(); + } + streams[i] = null; + } + } + } + + private void releaseOrphanEmbeddedStreams( + TrackSelection[] selections, SampleStream[] streams, int[] streamIndexToTrackGroupIndex) { + for (int i = 0; i < selections.length; i++) { + if (streams[i] instanceof EmptySampleStream || streams[i] instanceof EmbeddedSampleStream) { + // We need to release an embedded stream if the corresponding primary stream is released. + int primaryStreamIndex = getPrimaryStreamIndex(i, streamIndexToTrackGroupIndex); + boolean mayRetainStream; + if (primaryStreamIndex == C.INDEX_UNSET) { + // If the corresponding primary stream is not selected, we may retain an existing + // EmptySampleStream. + mayRetainStream = streams[i] instanceof EmptySampleStream; + } else { + // If the corresponding primary stream is selected, we may retain the embedded stream if + // the stream's parent still matches. + mayRetainStream = + (streams[i] instanceof EmbeddedSampleStream) + && ((EmbeddedSampleStream) streams[i]).parent == streams[primaryStreamIndex]; + } + if (!mayRetainStream) { + if (streams[i] instanceof EmbeddedSampleStream) { + ((EmbeddedSampleStream) streams[i]).release(); + } + streams[i] = null; + } + } + } + } + + private void selectNewStreams( + TrackSelection[] selections, + SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs, + int[] streamIndexToTrackGroupIndex) { + // Create newly selected primary and event streams. + for (int i = 0; i < selections.length; i++) { + if (streams[i] == null && selections[i] != null) { + streamResetFlags[i] = true; + int trackGroupIndex = streamIndexToTrackGroupIndex[i]; + TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; + if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) { + streams[i] = buildSampleStream(trackGroupInfo, selections[i], positionUs); + } else if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_MANIFEST_EVENTS) { + EventStream eventStream = eventStreams.get(trackGroupInfo.eventStreamGroupIndex); + Format format = selections[i].getTrackGroup().getFormat(0); + streams[i] = new EventSampleStream(eventStream, format, manifest.dynamic); + } + } + } + // Create newly selected embedded streams from the corresponding primary stream. Note that this + // second pass is needed because the primary stream may not have been created yet in a first + // pass if the index of the primary stream is greater than the index of the embedded stream. + for (int i = 0; i < selections.length; i++) { + if (streams[i] == null && selections[i] != null) { + int trackGroupIndex = streamIndexToTrackGroupIndex[i]; + TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; + if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_EMBEDDED) { + int primaryStreamIndex = getPrimaryStreamIndex(i, streamIndexToTrackGroupIndex); + if (primaryStreamIndex == C.INDEX_UNSET) { + // If an embedded track is selected without the corresponding primary track, create an + // empty sample stream instead. + streams[i] = new EmptySampleStream(); + } else { + streams[i] = + ((ChunkSampleStream) streams[primaryStreamIndex]) + .selectEmbeddedTrack(positionUs, trackGroupInfo.trackType); + } + } + } + } + } + + private int getPrimaryStreamIndex(int embeddedStreamIndex, int[] streamIndexToTrackGroupIndex) { + int embeddedTrackGroupIndex = streamIndexToTrackGroupIndex[embeddedStreamIndex]; + if (embeddedTrackGroupIndex == C.INDEX_UNSET) { + return C.INDEX_UNSET; + } + int primaryTrackGroupIndex = trackGroupInfos[embeddedTrackGroupIndex].primaryTrackGroupIndex; + for (int i = 0; i < streamIndexToTrackGroupIndex.length; i++) { + int trackGroupIndex = streamIndexToTrackGroupIndex[i]; + if (trackGroupIndex == primaryTrackGroupIndex + && trackGroupInfos[trackGroupIndex].trackGroupCategory + == TrackGroupInfo.CATEGORY_PRIMARY) { + return i; + } + } + return C.INDEX_UNSET; + } + private static Pair buildTrackGroups( List adaptationSets, List eventStreams) { int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets); @@ -560,7 +602,8 @@ import java.util.List; elapsedRealtimeOffset, enableEventMessageTrack, enableCea608Track, - trackPlayerEmsgHandler); + trackPlayerEmsgHandler, + transferListener); ChunkSampleStream stream = new ChunkSampleStream<>( trackGroupInfo.trackType, @@ -570,7 +613,7 @@ import java.util.List; this, allocator, positionUs, - minLoadableRetryCount, + loadErrorHandlingPolicy, eventDispatcher); synchronized (this) { // The map is also accessed on the loading thread so synchronize access. @@ -622,12 +665,6 @@ import java.util.List; return new ChunkSampleStream[length]; } - private static void releaseIfEmbeddedSampleStream(SampleStream sampleStream) { - if (sampleStream instanceof EmbeddedSampleStream) { - ((EmbeddedSampleStream) sampleStream).release(); - } - } - private static final class TrackGroupInfo { @Retention(RetentionPolicy.SOURCE) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 7b854e9d29..ede757aed0 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -37,14 +37,19 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispat import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback; +import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.Loader; +import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.BufferedReader; @@ -74,11 +79,22 @@ public final class DashMediaSource extends BaseMediaSource { private @Nullable ParsingLoadable.Parser manifestParser; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private int minLoadableRetryCount; + private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private long livePresentationDelayMs; + private boolean livePresentationDelayOverridesManifest; private boolean isCreateCalled; private @Nullable Object tag; + /** + * Creates a new factory for {@link DashMediaSource}s. + * + * @param dataSourceFactory A factory for {@link DataSource} instances that will be used to load + * manifest and media data. + */ + public Factory(DataSource.Factory dataSourceFactory) { + this(new DefaultDashChunkSource.Factory(dataSourceFactory), dataSourceFactory); + } + /** * Creates a new factory for {@link DashMediaSource}s. * @@ -93,8 +109,8 @@ public final class DashMediaSource extends BaseMediaSource { @Nullable DataSource.Factory manifestDataSourceFactory) { this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; - minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; - livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS; + loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); + livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } @@ -114,34 +130,69 @@ public final class DashMediaSource extends BaseMediaSource { } /** - * Sets the minimum number of times to retry if a loading error occurs. The default value is - * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * Sets the minimum number of times to retry if a loading error occurs. See {@link + * #setLoadErrorHandlingPolicy} for the default value. + * + *

    Calling this method is equivalent to calling {@link #setLoadErrorHandlingPolicy} with + * {@link DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy(int) + * DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)} * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead. */ + @Deprecated public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)); + } + + /** + * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link + * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. + * + *

    Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}. + * + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { Assertions.checkState(!isCreateCalled); - this.minLoadableRetryCount = minLoadableRetryCount; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; return this; } + /** @deprecated Use {@link #setLivePresentationDelayMs(long, boolean)}. */ + @Deprecated + @SuppressWarnings("deprecation") + public Factory setLivePresentationDelayMs(long livePresentationDelayMs) { + if (livePresentationDelayMs == DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS) { + return setLivePresentationDelayMs(DEFAULT_LIVE_PRESENTATION_DELAY_MS, false); + } else { + return setLivePresentationDelayMs(livePresentationDelayMs, true); + } + } + /** * Sets the duration in milliseconds by which the default start position should precede the end - * of the live window for live playbacks. The default value is {@link - * #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS}. + * of the live window for live playbacks. The {@code overridesManifest} parameter specifies + * whether the value is used in preference to one in the manifest, if present. The default value + * is {@link #DEFAULT_LIVE_PRESENTATION_DELAY_MS}, and by default {@code overridesManifest} is + * false. * * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the - * default start position should precede the end of the live window. Use {@link - * #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by the - * manifest, if present. + * default start position should precede the end of the live window. + * @param overridesManifest Whether the value is used in preference to one in the manifest, if + * present. * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setLivePresentationDelayMs(long livePresentationDelayMs) { + public Factory setLivePresentationDelayMs( + long livePresentationDelayMs, boolean overridesManifest) { Assertions.checkState(!isCreateCalled); this.livePresentationDelayMs = livePresentationDelayMs; + this.livePresentationDelayOverridesManifest = overridesManifest; return this; } @@ -196,8 +247,9 @@ public final class DashMediaSource extends BaseMediaSource { /* manifestParser= */ null, chunkSourceFactory, compositeSequenceableLoaderFactory, - minLoadableRetryCount, + loadErrorHandlingPolicy, livePresentationDelayMs, + livePresentationDelayOverridesManifest, tag); } @@ -236,8 +288,9 @@ public final class DashMediaSource extends BaseMediaSource { manifestParser, chunkSourceFactory, compositeSequenceableLoaderFactory, - minLoadableRetryCount, + loadErrorHandlingPolicy, livePresentationDelayMs, + livePresentationDelayOverridesManifest, tag); } @@ -264,21 +317,16 @@ public final class DashMediaSource extends BaseMediaSource { } /** - * The default minimum number of times to retry loading data prior to failing. + * The default presentation delay for live streams. The presentation delay is the duration by + * which the default start position precedes the end of the live window. */ - public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; - /** - * A constant indicating that the presentation delay for live streams should be set to - * {@link DashManifest#suggestedPresentationDelayMs} if specified by the manifest, or - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS} otherwise. The presentation delay is the - * duration by which the default start position precedes the end of the live window. - */ - public static final long DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS = -1; - /** - * A fixed default presentation delay for live streams. The presentation delay is the duration - * by which the default start position precedes the end of the live window. - */ - public static final long DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS = 30000; + public static final long DEFAULT_LIVE_PRESENTATION_DELAY_MS = 30000; + /** @deprecated Use {@link #DEFAULT_LIVE_PRESENTATION_DELAY_MS}. */ + @Deprecated + public static final long DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS = + DEFAULT_LIVE_PRESENTATION_DELAY_MS; + /** @deprecated Use of this parameter is no longer necessary. */ + @Deprecated public static final long DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS = -1; /** * The interval in milliseconds between invocations of {@link @@ -297,8 +345,9 @@ public final class DashMediaSource extends BaseMediaSource { private final DataSource.Factory manifestDataSourceFactory; private final DashChunkSource.Factory chunkSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private final int minLoadableRetryCount; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long livePresentationDelayMs; + private final boolean livePresentationDelayOverridesManifest; private final EventDispatcher manifestEventDispatcher; private final ParsingLoadable.Parser manifestParser; private final ManifestCallback manifestCallback; @@ -312,6 +361,7 @@ public final class DashMediaSource extends BaseMediaSource { private DataSource dataSource; private Loader loader; + private @Nullable TransferListener mediaTransferListener; private IOException manifestFatalError; private Handler handler; @@ -340,12 +390,17 @@ public final class DashMediaSource extends BaseMediaSource { * @deprecated Use {@link Factory} instead. */ @Deprecated + @SuppressWarnings("deprecation") public DashMediaSource( DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, Handler eventHandler, MediaSourceEventListener eventListener) { - this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, + this( + manifest, + chunkSourceFactory, + DefaultLoadErrorHandlingPolicy.DEFAULT_MIN_LOADABLE_RETRY_COUNT, + eventHandler, eventListener); } @@ -373,8 +428,9 @@ public final class DashMediaSource extends BaseMediaSource { /* manifestParser= */ null, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), - minLoadableRetryCount, - DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, + new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), + DEFAULT_LIVE_PRESENTATION_DELAY_MS, + /* livePresentationDelayOverridesManifest= */ false, /* tag= */ null); if (eventHandler != null && eventListener != null) { addEventListener(eventHandler, eventListener); @@ -394,15 +450,21 @@ public final class DashMediaSource extends BaseMediaSource { * @deprecated Use {@link Factory} instead. */ @Deprecated + @SuppressWarnings("deprecation") public DashMediaSource( Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, Handler eventHandler, MediaSourceEventListener eventListener) { - this(manifestUri, manifestDataSourceFactory, chunkSourceFactory, - DEFAULT_MIN_LOADABLE_RETRY_COUNT, DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, - eventHandler, eventListener); + this( + manifestUri, + manifestDataSourceFactory, + chunkSourceFactory, + DefaultLoadErrorHandlingPolicy.DEFAULT_MIN_LOADABLE_RETRY_COUNT, + DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, + eventHandler, + eventListener); } /** @@ -423,6 +485,7 @@ public final class DashMediaSource extends BaseMediaSource { * @deprecated Use {@link Factory} instead. */ @Deprecated + @SuppressWarnings("deprecation") public DashMediaSource( Uri manifestUri, DataSource.Factory manifestDataSourceFactory, @@ -431,8 +494,15 @@ public final class DashMediaSource extends BaseMediaSource { long livePresentationDelayMs, Handler eventHandler, MediaSourceEventListener eventListener) { - this(manifestUri, manifestDataSourceFactory, new DashManifestParser(), chunkSourceFactory, - minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); + this( + manifestUri, + manifestDataSourceFactory, + new DashManifestParser(), + chunkSourceFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, + eventListener); } /** @@ -454,6 +524,7 @@ public final class DashMediaSource extends BaseMediaSource { * @deprecated Use {@link Factory} instead. */ @Deprecated + @SuppressWarnings("deprecation") public DashMediaSource( Uri manifestUri, DataSource.Factory manifestDataSourceFactory, @@ -470,8 +541,11 @@ public final class DashMediaSource extends BaseMediaSource { manifestParser, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), - minLoadableRetryCount, - livePresentationDelayMs, + new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), + livePresentationDelayMs == DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS + ? DEFAULT_LIVE_PRESENTATION_DELAY_MS + : livePresentationDelayMs, + livePresentationDelayMs != DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, /* tag= */ null); if (eventHandler != null && eventListener != null) { addEventListener(eventHandler, eventListener); @@ -485,8 +559,9 @@ public final class DashMediaSource extends BaseMediaSource { ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, - int minLoadableRetryCount, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, long livePresentationDelayMs, + boolean livePresentationDelayOverridesManifest, @Nullable Object tag) { this.initialManifestUri = manifestUri; this.manifest = manifest; @@ -494,8 +569,9 @@ public final class DashMediaSource extends BaseMediaSource { this.manifestDataSourceFactory = manifestDataSourceFactory; this.manifestParser = manifestParser; this.chunkSourceFactory = chunkSourceFactory; - this.minLoadableRetryCount = minLoadableRetryCount; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.livePresentationDelayMs = livePresentationDelayMs; + this.livePresentationDelayOverridesManifest = livePresentationDelayOverridesManifest; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.tag = tag; sideloadedManifest = manifest != null; @@ -543,7 +619,11 @@ public final class DashMediaSource extends BaseMediaSource { // MediaSource implementation. @Override - public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + public void prepareSourceInternal( + ExoPlayer player, + boolean isTopLevelSource, + @Nullable TransferListener mediaTransferListener) { + this.mediaTransferListener = mediaTransferListener; if (sideloadedManifest) { processManifest(false); } else { @@ -570,7 +650,8 @@ public final class DashMediaSource extends BaseMediaSource { manifest, periodIndex, chunkSourceFactory, - minLoadableRetryCount, + mediaTransferListener, + loadErrorHandlingPolicy, periodEventDispatcher, elapsedRealtimeOffsetMs, manifestLoadErrorThrower, @@ -637,6 +718,7 @@ public final class DashMediaSource extends BaseMediaSource { long elapsedRealtimeMs, long loadDurationMs) { manifestEventDispatcher.loadCompleted( loadable.dataSpec, + loadable.getUri(), loadable.type, elapsedRealtimeMs, loadDurationMs, @@ -679,7 +761,8 @@ public final class DashMediaSource extends BaseMediaSource { } if (isManifestStale) { - if (staleManifestReloadAttempt++ < minLoadableRetryCount) { + if (staleManifestReloadAttempt++ + < loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)) { scheduleManifestRefresh(getManifestLoadRetryDelayMillis()); } else { manifestFatalError = new DashManifestStaleException(); @@ -698,7 +781,9 @@ public final class DashMediaSource extends BaseMediaSource { synchronized (manifestUriLock) { // This condition checks that replaceManifestUri wasn't called between the start and end of // this load. If it was, we ignore the manifest location and prefer the manual replacement. - if (loadable.dataSpec.uri == manifestUri) { + @SuppressWarnings("ReferenceEquality") + boolean isSameUriInstance = loadable.dataSpec.uri == manifestUri; + if (isSameUriInstance) { manifestUri = manifest.location; } } @@ -716,8 +801,7 @@ public final class DashMediaSource extends BaseMediaSource { } } - /* package */ @Loader.RetryAction - int onManifestLoadError( + /* package */ LoadErrorAction onManifestLoadError( ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, @@ -725,6 +809,7 @@ public final class DashMediaSource extends BaseMediaSource { boolean isFatal = error instanceof ParserException; manifestEventDispatcher.loadError( loadable.dataSpec, + loadable.getUri(), loadable.type, elapsedRealtimeMs, loadDurationMs, @@ -738,6 +823,7 @@ public final class DashMediaSource extends BaseMediaSource { long elapsedRealtimeMs, long loadDurationMs) { manifestEventDispatcher.loadCompleted( loadable.dataSpec, + loadable.getUri(), loadable.type, elapsedRealtimeMs, loadDurationMs, @@ -745,14 +831,14 @@ public final class DashMediaSource extends BaseMediaSource { onUtcTimestampResolved(loadable.getResult() - elapsedRealtimeMs); } - /* package */ @Loader.RetryAction - int onUtcTimestampLoadError( + /* package */ LoadErrorAction onUtcTimestampLoadError( ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { manifestEventDispatcher.loadError( loadable.dataSpec, + loadable.getUri(), loadable.type, elapsedRealtimeMs, loadDurationMs, @@ -767,6 +853,7 @@ public final class DashMediaSource extends BaseMediaSource { long loadDurationMs) { manifestEventDispatcher.loadCanceled( loadable.dataSpec, + loadable.getUri(), loadable.type, elapsedRealtimeMs, loadDurationMs, @@ -869,9 +956,9 @@ public final class DashMediaSource extends BaseMediaSource { long windowDefaultStartPositionUs = 0; if (manifest.dynamic) { long presentationDelayForManifestMs = livePresentationDelayMs; - if (presentationDelayForManifestMs == DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS) { - presentationDelayForManifestMs = manifest.suggestedPresentationDelayMs != C.TIME_UNSET - ? manifest.suggestedPresentationDelayMs : DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS; + if (!livePresentationDelayOverridesManifest + && manifest.suggestedPresentationDelayMs != C.TIME_UNSET) { + presentationDelayForManifestMs = manifest.suggestedPresentationDelayMs; } // Snap the default position to the start of the segment containing it. windowDefaultStartPositionUs = windowDurationUs - C.msToUs(presentationDelayForManifestMs); @@ -944,7 +1031,7 @@ public final class DashMediaSource extends BaseMediaSource { startLoading( new ParsingLoadable<>(dataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser), manifestCallback, - minLoadableRetryCount); + loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MANIFEST)); } private long getManifestLoadRetryDelayMillis() { @@ -954,7 +1041,8 @@ public final class DashMediaSource extends BaseMediaSource { private void startLoading(ParsingLoadable loadable, Loader.Callback> callback, int minRetryCount) { long elapsedRealtimeMs = loader.startLoading(loadable, callback, minRetryCount); - manifestEventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); + manifestEventDispatcher.loadStarted( + loadable.dataSpec, loadable.dataSpec.uri, loadable.type, elapsedRealtimeMs); } private long getNowUnixTimeUs() { @@ -974,8 +1062,25 @@ public final class DashMediaSource extends BaseMediaSource { long availableEndTimeUs = Long.MAX_VALUE; boolean isIndexExplicit = false; boolean seenEmptyIndex = false; + + boolean haveAudioVideoAdaptationSets = false; for (int i = 0; i < adaptationSetCount; i++) { - DashSegmentIndex index = period.adaptationSets.get(i).representations.get(0).getIndex(); + int type = period.adaptationSets.get(i).type; + if (type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO) { + haveAudioVideoAdaptationSets = true; + break; + } + } + + for (int i = 0; i < adaptationSetCount; i++) { + AdaptationSet adaptationSet = period.adaptationSets.get(i); + // Exclude text adaptation sets from duration calculations, if we have at least one audio + // or video adaptation set. See: https://github.com/google/ExoPlayer/issues/4029 + if (haveAudioVideoAdaptationSets && adaptationSet.type == C.TRACK_TYPE_TEXT) { + continue; + } + + DashSegmentIndex index = adaptationSet.representations.get(0).getIndex(); if (index == null) { return new PeriodSeekInfo(true, 0, durationUs); } @@ -1051,10 +1156,9 @@ public final class DashMediaSource extends BaseMediaSource { @Override public Period getPeriod(int periodIndex, Period period, boolean setIdentifiers) { - Assertions.checkIndex(periodIndex, 0, manifest.getPeriodCount()); + Assertions.checkIndex(periodIndex, 0, getPeriodCount()); Object id = setIdentifiers ? manifest.getPeriod(periodIndex).id : null; - Object uid = setIdentifiers ? firstPeriodId - + Assertions.checkIndex(periodIndex, 0, manifest.getPeriodCount()) : null; + Object uid = setIdentifiers ? (firstPeriodId + periodIndex) : null; return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex), C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs) - offsetInFirstPeriodUs); @@ -1081,7 +1185,7 @@ public final class DashMediaSource extends BaseMediaSource { windowDefaultStartPositionUs, windowDurationUs, /* firstPeriodIndex= */ 0, - manifest.getPeriodCount() - 1, + /* lastPeriodIndex= */ getPeriodCount() - 1, offsetInFirstPeriodUs); } @@ -1091,8 +1195,8 @@ public final class DashMediaSource extends BaseMediaSource { return C.INDEX_UNSET; } int periodId = (int) uid; - return periodId < firstPeriodId || periodId >= firstPeriodId + getPeriodCount() - ? C.INDEX_UNSET : (periodId - firstPeriodId); + int periodIndex = periodId - firstPeriodId; + return periodIndex < 0 || periodIndex >= getPeriodCount() ? C.INDEX_UNSET : periodIndex; } private long getAdjustedWindowDefaultStartPositionUs(long defaultPositionProjectionUs) { @@ -1137,6 +1241,11 @@ public final class DashMediaSource extends BaseMediaSource { - defaultStartPositionInPeriodUs; } + @Override + public Object getUidOfPeriod(int periodIndex) { + Assertions.checkIndex(periodIndex, 0, getPeriodCount()); + return firstPeriodId + periodIndex; + } } private final class DefaultPlayerEmsgCallback implements PlayerEmsgCallback { @@ -1172,11 +1281,12 @@ public final class DashMediaSource extends BaseMediaSource { } @Override - public @Loader.RetryAction int onLoadError( + public LoadErrorAction onLoadError( ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, - IOException error) { + IOException error, + int errorCount) { return onManifestLoadError(loadable, elapsedRealtimeMs, loadDurationMs, error); } @@ -1197,11 +1307,12 @@ public final class DashMediaSource extends BaseMediaSource { } @Override - public @Loader.RetryAction int onLoadError( + public LoadErrorAction onLoadError( ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, - IOException error) { + IOException error, + int errorCount) { return onUtcTimestampLoadError(loadable, elapsedRealtimeMs, loadDurationMs, error); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java index 6b481df46d..743462bd89 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java @@ -54,7 +54,7 @@ public final class DashUtil { */ public static DashManifest loadManifest(DataSource dataSource, Uri uri) throws IOException { - return ParsingLoadable.load(dataSource, new DashManifestParser(), uri); + return ParsingLoadable.load(dataSource, new DashManifestParser(), uri, C.DATA_TYPE_MANIFEST); } /** diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java index 078305a687..3eca7892c4 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java @@ -25,12 +25,15 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri; public final class DashWrappingSegmentIndex implements DashSegmentIndex { private final ChunkIndex chunkIndex; + private final long timeOffsetUs; /** * @param chunkIndex The {@link ChunkIndex} to wrap. + * @param timeOffsetUs An offset to subtract from the times in the wrapped index, in microseconds. */ - public DashWrappingSegmentIndex(ChunkIndex chunkIndex) { + public DashWrappingSegmentIndex(ChunkIndex chunkIndex, long timeOffsetUs) { this.chunkIndex = chunkIndex; + this.timeOffsetUs = timeOffsetUs; } @Override @@ -45,7 +48,7 @@ public final class DashWrappingSegmentIndex implements DashSegmentIndex { @Override public long getTimeUs(long segmentNum) { - return chunkIndex.timesUs[(int) segmentNum]; + return chunkIndex.timesUs[(int) segmentNum] - timeOffsetUs; } @Override @@ -61,7 +64,7 @@ public final class DashWrappingSegmentIndex implements DashSegmentIndex { @Override public long getSegmentNum(long timeUs, long periodDurationUs) { - return chunkIndex.getChunkIndex(timeUs); + return chunkIndex.getChunkIndex(timeUs + timeOffsetUs); } @Override diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 4cb14d6614..2574b0a12e 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.dash; import android.net.Uri; import android.os.SystemClock; +import android.support.annotation.CheckResult; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -29,13 +30,14 @@ import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer2.extractor.rawcc.RawCcExtractor; import com.google.android.exoplayer2.source.BehindLiveWindowException; +import com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator; import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper; import com.google.android.exoplayer2.source.chunk.ChunkHolder; -import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk; import com.google.android.exoplayer2.source.chunk.InitializationChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk; import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; @@ -47,6 +49,7 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -84,8 +87,12 @@ public class DefaultDashChunkSource implements DashChunkSource { long elapsedRealtimeOffsetMs, boolean enableEventMessageTrack, boolean enableCea608Track, - @Nullable PlayerTrackEmsgHandler playerEmsgHandler) { + @Nullable PlayerTrackEmsgHandler playerEmsgHandler, + @Nullable TransferListener transferListener) { DataSource dataSource = dataSourceFactory.createDataSource(); + if (transferListener != null) { + dataSource.addTransferListener(transferListener); + } return new DefaultDashChunkSource( manifestLoaderErrorThrower, manifest, @@ -209,7 +216,8 @@ public class DefaultDashChunkSource implements DashChunkSource { List representations = getRepresentations(); for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); - representationHolders[i].updateRepresentation(periodDurationUs, representation); + representationHolders[i] = + representationHolders[i].copyWithNewRepresentation(periodDurationUs, representation); } } catch (BehindLiveWindowException e) { fatalError = e; @@ -234,7 +242,10 @@ public class DefaultDashChunkSource implements DashChunkSource { } @Override - public void getNextChunk(MediaChunk previous, long playbackPositionUs, long loadPositionUs, + public void getNextChunk( + long playbackPositionUs, + long loadPositionUs, + List queue, ChunkHolder out) { if (fatalError != null) { return; @@ -307,11 +318,11 @@ public class DefaultDashChunkSource implements DashChunkSource { updateLiveEdgeTimeUs(representationHolder, lastAvailableSegmentNum); long segmentNum; - if (previous == null) { + if (queue.isEmpty()) { segmentNum = Util.constrainValue(representationHolder.getSegmentNum(loadPositionUs), firstAvailableSegmentNum, lastAvailableSegmentNum); } else { - segmentNum = previous.getNextChunkIndex(); + segmentNum = queue.get(queue.size() - 1).getNextChunkIndex(); if (segmentNum < firstAvailableSegmentNum) { // This is before the first chunk in the current manifest. fatalError = new BehindLiveWindowException(); @@ -328,7 +339,7 @@ public class DefaultDashChunkSource implements DashChunkSource { int maxSegmentCount = (int) Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1); - long seekTimeUs = previous == null ? loadPositionUs : C.TIME_UNSET; + long seekTimeUs = queue.isEmpty() ? loadPositionUs : C.TIME_UNSET; out.chunk = newMediaChunk( representationHolder, @@ -346,15 +357,19 @@ public class DefaultDashChunkSource implements DashChunkSource { public void onChunkLoadCompleted(Chunk chunk) { if (chunk instanceof InitializationChunk) { InitializationChunk initializationChunk = (InitializationChunk) chunk; - RepresentationHolder representationHolder = - representationHolders[trackSelection.indexOf(initializationChunk.trackFormat)]; + int trackIndex = trackSelection.indexOf(initializationChunk.trackFormat); + RepresentationHolder representationHolder = representationHolders[trackIndex]; // The null check avoids overwriting an index obtained from the manifest with one obtained // from the stream. If the manifest defines an index then the stream shouldn't, but in cases // where it does we should ignore it. if (representationHolder.segmentIndex == null) { SeekMap seekMap = representationHolder.extractorWrapper.getSeekMap(); if (seekMap != null) { - representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap); + representationHolders[trackIndex] = + representationHolder.copyWithNewSegmentIndex( + new DashWrappingSegmentIndex( + (ChunkIndex) seekMap, + representationHolder.representation.presentationTimeOffsetUs)); } } } @@ -364,7 +379,8 @@ public class DefaultDashChunkSource implements DashChunkSource { } @Override - public boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e) { + public boolean onChunkLoadError( + Chunk chunk, boolean cancelable, Exception e, long blacklistDurationMs) { if (!cancelable) { return false; } @@ -387,9 +403,8 @@ public class DefaultDashChunkSource implements DashChunkSource { } } } - // Blacklist if appropriate. - return ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection, - trackSelection.indexOf(chunk.trackFormat), e); + return blacklistDurationMs != C.TIME_UNSET + && trackSelection.blacklist(trackSelection.indexOf(chunk.trackFormat), blacklistDurationMs); } // Internal methods. @@ -422,9 +437,14 @@ public class DefaultDashChunkSource implements DashChunkSource { return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; } - protected static Chunk newInitializationChunk(RepresentationHolder representationHolder, - DataSource dataSource, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, RangedUri initializationUri, RangedUri indexUri) { + protected Chunk newInitializationChunk( + RepresentationHolder representationHolder, + DataSource dataSource, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + RangedUri initializationUri, + RangedUri indexUri) { RangedUri requestUri; String baseUrl = representationHolder.representation.baseUrl; if (initializationUri != null) { @@ -443,7 +463,7 @@ public class DefaultDashChunkSource implements DashChunkSource { trackSelectionReason, trackSelectionData, representationHolder.extractorWrapper); } - protected static Chunk newMediaChunk( + protected Chunk newMediaChunk( RepresentationHolder representationHolder, DataSource dataSource, int trackType, @@ -497,18 +517,57 @@ public class DefaultDashChunkSource implements DashChunkSource { // Protected classes. - /** - * Holds information about a single {@link Representation}. - */ + /** {@link MediaChunkIterator} wrapping a {@link RepresentationHolder}. */ + protected static final class RepresentationSegmentIterator extends BaseMediaChunkIterator { + + private final RepresentationHolder representationHolder; + + /** + * Creates iterator. + * + * @param representation The {@link RepresentationHolder} to wrap. + * @param segmentNum The number of the segment this iterator will be pointing to initially. + * @param lastAvailableSegmentNum The number of the last available segment. + */ + public RepresentationSegmentIterator( + RepresentationHolder representation, long segmentNum, long lastAvailableSegmentNum) { + super(/* fromIndex= */ segmentNum, /* toIndex= */ lastAvailableSegmentNum); + this.representationHolder = representation; + } + + @Override + public DataSpec getDataSpec() { + checkInBounds(); + Representation representation = representationHolder.representation; + RangedUri segmentUri = representationHolder.getSegmentUrl(getCurrentIndex()); + Uri resolvedUri = segmentUri.resolveUri(representation.baseUrl); + String cacheKey = representation.getCacheKey(); + return new DataSpec(resolvedUri, segmentUri.start, segmentUri.length, cacheKey); + } + + @Override + public long getChunkStartTimeUs() { + checkInBounds(); + return representationHolder.getSegmentStartTimeUs(getCurrentIndex()); + } + + @Override + public long getChunkEndTimeUs() { + checkInBounds(); + return representationHolder.getSegmentEndTimeUs(getCurrentIndex()); + } + } + + /** Holds information about a snapshot of a single {@link Representation}. */ protected static final class RepresentationHolder { - /* package */ final ChunkExtractorWrapper extractorWrapper; + /* package */ final @Nullable ChunkExtractorWrapper extractorWrapper; - public Representation representation; - public DashSegmentIndex segmentIndex; + public final Representation representation; + public final @Nullable DashSegmentIndex segmentIndex; - private long periodDurationUs; - private long segmentNumShift; + private final long periodDurationUs; + private final long segmentNumShift; /* package */ RepresentationHolder( long periodDurationUs, @@ -517,80 +576,86 @@ public class DefaultDashChunkSource implements DashChunkSource { boolean enableEventMessageTrack, boolean enableCea608Track, TrackOutput playerEmsgTrackOutput) { - this.periodDurationUs = periodDurationUs; - this.representation = representation; - String containerMimeType = representation.format.containerMimeType; - if (mimeTypeIsRawText(containerMimeType)) { - extractorWrapper = null; - } else { - Extractor extractor; - if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { - extractor = new RawCcExtractor(representation.format); - } else if (mimeTypeIsWebm(containerMimeType)) { - extractor = new MatroskaExtractor(MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES); - } else { - int flags = 0; - if (enableEventMessageTrack) { - flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK; - } - // TODO: Use caption format information from the manifest if available. - List closedCaptionFormats = enableCea608Track - ? Collections.singletonList( - Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null)) - : Collections.emptyList(); - - extractor = - new FragmentedMp4Extractor( - flags, null, null, null, closedCaptionFormats, playerEmsgTrackOutput); - } - // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, - // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. - extractorWrapper = new ChunkExtractorWrapper(extractor, trackType, representation.format); - } - segmentIndex = representation.getIndex(); + this( + periodDurationUs, + representation, + createExtractorWrapper( + trackType, + representation, + enableEventMessageTrack, + enableCea608Track, + playerEmsgTrackOutput), + /* segmentNumShift= */ 0, + representation.getIndex()); } - /* package */ void updateRepresentation(long newPeriodDurationUs, - Representation newRepresentation) throws BehindLiveWindowException { + private RepresentationHolder( + long periodDurationUs, + Representation representation, + @Nullable ChunkExtractorWrapper extractorWrapper, + long segmentNumShift, + @Nullable DashSegmentIndex segmentIndex) { + this.periodDurationUs = periodDurationUs; + this.representation = representation; + this.segmentNumShift = segmentNumShift; + this.extractorWrapper = extractorWrapper; + this.segmentIndex = segmentIndex; + } + + @CheckResult + /* package */ RepresentationHolder copyWithNewRepresentation( + long newPeriodDurationUs, Representation newRepresentation) + throws BehindLiveWindowException { DashSegmentIndex oldIndex = representation.getIndex(); DashSegmentIndex newIndex = newRepresentation.getIndex(); - periodDurationUs = newPeriodDurationUs; - representation = newRepresentation; if (oldIndex == null) { // Segment numbers cannot shift if the index isn't defined by the manifest. - return; + return new RepresentationHolder( + newPeriodDurationUs, newRepresentation, extractorWrapper, segmentNumShift, oldIndex); } - segmentIndex = newIndex; if (!oldIndex.isExplicit()) { // Segment numbers cannot shift if the index isn't explicit. - return; + return new RepresentationHolder( + newPeriodDurationUs, newRepresentation, extractorWrapper, segmentNumShift, newIndex); } - int oldIndexSegmentCount = oldIndex.getSegmentCount(periodDurationUs); + int oldIndexSegmentCount = oldIndex.getSegmentCount(newPeriodDurationUs); if (oldIndexSegmentCount == 0) { // Segment numbers cannot shift if the old index was empty. - return; + return new RepresentationHolder( + newPeriodDurationUs, newRepresentation, extractorWrapper, segmentNumShift, newIndex); } long oldIndexLastSegmentNum = oldIndex.getFirstSegmentNum() + oldIndexSegmentCount - 1; - long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum) - + oldIndex.getDurationUs(oldIndexLastSegmentNum, periodDurationUs); + long oldIndexEndTimeUs = + oldIndex.getTimeUs(oldIndexLastSegmentNum) + + oldIndex.getDurationUs(oldIndexLastSegmentNum, newPeriodDurationUs); long newIndexFirstSegmentNum = newIndex.getFirstSegmentNum(); long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum); + long newSegmentNumShift = segmentNumShift; if (oldIndexEndTimeUs == newIndexStartTimeUs) { // The new index continues where the old one ended, with no overlap. - segmentNumShift += oldIndexLastSegmentNum + 1 - newIndexFirstSegmentNum; + newSegmentNumShift += oldIndexLastSegmentNum + 1 - newIndexFirstSegmentNum; } else if (oldIndexEndTimeUs < newIndexStartTimeUs) { // There's a gap between the old index and the new one which means we've slipped behind the // live window and can't proceed. throw new BehindLiveWindowException(); } else { // The new index overlaps with the old one. - segmentNumShift += oldIndex.getSegmentNum(newIndexStartTimeUs, periodDurationUs) - - newIndexFirstSegmentNum; + newSegmentNumShift += + oldIndex.getSegmentNum(newIndexStartTimeUs, newPeriodDurationUs) + - newIndexFirstSegmentNum; } + return new RepresentationHolder( + newPeriodDurationUs, newRepresentation, extractorWrapper, newSegmentNumShift, newIndex); + } + + @CheckResult + /* package */ RepresentationHolder copyWithNewSegmentIndex(DashSegmentIndex segmentIndex) { + return new RepresentationHolder( + periodDurationUs, representation, extractorWrapper, segmentNumShift, segmentIndex); } public long getFirstSegmentNum() { @@ -626,5 +691,40 @@ public class DefaultDashChunkSource implements DashChunkSource { private static boolean mimeTypeIsRawText(String mimeType) { return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType); } + + private static @Nullable ChunkExtractorWrapper createExtractorWrapper( + int trackType, + Representation representation, + boolean enableEventMessageTrack, + boolean enableCea608Track, + TrackOutput playerEmsgTrackOutput) { + String containerMimeType = representation.format.containerMimeType; + if (mimeTypeIsRawText(containerMimeType)) { + return null; + } + Extractor extractor; + if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { + extractor = new RawCcExtractor(representation.format); + } else if (mimeTypeIsWebm(containerMimeType)) { + extractor = new MatroskaExtractor(MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES); + } else { + int flags = 0; + if (enableEventMessageTrack) { + flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK; + } + // TODO: Use caption format information from the manifest if available. + List closedCaptionFormats = + enableCea608Track + ? Collections.singletonList( + Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null)) + : Collections.emptyList(); + extractor = + new FragmentedMp4Extractor( + flags, null, null, null, closedCaptionFormats, playerEmsgTrackOutput); + } + // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, + // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. + return new ChunkExtractorWrapper(extractor, trackType, representation.format); + } } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java index 7fef59f6a1..9f812b8e84 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java @@ -36,37 +36,53 @@ import java.io.IOException; private final EventMessageEncoder eventMessageEncoder; private long[] eventTimesUs; - private boolean eventStreamUpdatable; + private boolean eventStreamAppendable; private EventStream eventStream; private boolean isFormatSentDownstream; private int currentIndex; private long pendingSeekPositionUs; - EventSampleStream(EventStream eventStream, Format upstreamFormat, boolean eventStreamUpdatable) { + public EventSampleStream( + EventStream eventStream, Format upstreamFormat, boolean eventStreamAppendable) { this.upstreamFormat = upstreamFormat; this.eventStream = eventStream; eventMessageEncoder = new EventMessageEncoder(); pendingSeekPositionUs = C.TIME_UNSET; eventTimesUs = eventStream.presentationTimesUs; - updateEventStream(eventStream, eventStreamUpdatable); + updateEventStream(eventStream, eventStreamAppendable); } - void updateEventStream(EventStream eventStream, boolean eventStreamUpdatable) { + public String eventStreamId() { + return eventStream.id(); + } + + public void updateEventStream(EventStream eventStream, boolean eventStreamAppendable) { long lastReadPositionUs = currentIndex == 0 ? C.TIME_UNSET : eventTimesUs[currentIndex - 1]; - this.eventStreamUpdatable = eventStreamUpdatable; + this.eventStreamAppendable = eventStreamAppendable; this.eventStream = eventStream; this.eventTimesUs = eventStream.presentationTimesUs; if (pendingSeekPositionUs != C.TIME_UNSET) { seekToUs(pendingSeekPositionUs); } else if (lastReadPositionUs != C.TIME_UNSET) { - currentIndex = Util.binarySearchCeil(eventTimesUs, lastReadPositionUs, false, false); + currentIndex = + Util.binarySearchCeil( + eventTimesUs, lastReadPositionUs, /* inclusive= */ false, /* stayInBounds= */ false); } } - String eventStreamId() { - return eventStream.id(); + /** + * Seeks to the specified position in microseconds. + * + * @param positionUs The seek position in microseconds. + */ + public void seekToUs(long positionUs) { + currentIndex = + Util.binarySearchCeil( + eventTimesUs, positionUs, /* inclusive= */ true, /* stayInBounds= */ false); + boolean isPendingSeek = eventStreamAppendable && currentIndex == eventTimesUs.length; + pendingSeekPositionUs = isPendingSeek ? positionUs : C.TIME_UNSET; } @Override @@ -88,7 +104,7 @@ import java.io.IOException; return C.RESULT_FORMAT_READ; } if (currentIndex == eventTimesUs.length) { - if (!eventStreamUpdatable) { + if (!eventStreamAppendable) { buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); return C.RESULT_BUFFER_READ; } else { @@ -118,15 +134,4 @@ import java.io.IOException; return skipped; } - /** - * Seeks to the specified position in microseconds. - * - * @param positionUs The seek position in microseconds. - */ - public void seekToUs(long positionUs) { - currentIndex = Util.binarySearchCeil(eventTimesUs, positionUs, true, false); - boolean isPendingSeek = eventStreamUpdatable && currentIndex == eventTimesUs.length; - pendingSeekPositionUs = isPendingSeek ? positionUs : C.TIME_UNSET; - } - } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java index 1bb08c4398..a4a37cd904 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.Iterator; import java.util.Map; @@ -100,7 +101,6 @@ public final class PlayerEmsgHandler implements Handler.Callback { * messages that generate DASH media source events. * @param allocator An {@link Allocator} from which allocations can be obtained. */ - @SuppressWarnings("nullness") public PlayerEmsgHandler( DashManifest manifest, PlayerEmsgCallback playerEmsgCallback, Allocator allocator) { this.manifest = manifest; @@ -108,7 +108,7 @@ public final class PlayerEmsgHandler implements Handler.Callback { this.allocator = allocator; manifestPublishTimeToExpiryTimeUs = new TreeMap<>(); - handler = new Handler(this); + handler = Util.createHandler(/* callback= */ this); decoder = new EventMessageDecoder(); lastLoadedChunkEndTimeUs = C.TIME_UNSET; lastLoadedChunkEndTimeBeforeRefreshUs = C.TIME_UNSET; @@ -336,7 +336,7 @@ public final class PlayerEmsgHandler implements Handler.Callback { @Override public void sampleMetadata( - long timeUs, int flags, int size, int offset, CryptoData encryptionData) { + long timeUs, int flags, int size, int offset, @Nullable CryptoData encryptionData) { sampleQueue.sampleMetadata(timeUs, flags, size, offset, encryptionData); parseAndDiscardSamples(); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java index fd91a2f784..d962374745 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java @@ -69,12 +69,14 @@ public class AdaptationSet { this.id = id; this.type = type; this.representations = Collections.unmodifiableList(representations); - this.accessibilityDescriptors = accessibilityDescriptors == null - ? Collections.emptyList() - : Collections.unmodifiableList(accessibilityDescriptors); - this.supplementalProperties = supplementalProperties == null - ? Collections.emptyList() - : Collections.unmodifiableList(supplementalProperties); + this.accessibilityDescriptors = + accessibilityDescriptors == null + ? Collections.emptyList() + : Collections.unmodifiableList(accessibilityDescriptors); + this.supplementalProperties = + supplementalProperties == null + ? Collections.emptyList() + : Collections.unmodifiableList(supplementalProperties); } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index 639ad32d78..1fdb137be9 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.dash.manifest; import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.FilterableManifest; +import com.google.android.exoplayer2.offline.StreamKey; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; @@ -27,7 +28,7 @@ import java.util.List; * Represents a DASH media presentation description (mpd), as defined by ISO/IEC 23009-1:2014 * Section 5.3.1.2. */ -public class DashManifest implements FilterableManifest { +public class DashManifest implements FilterableManifest { /** * The {@code availabilityStartTime} value in milliseconds since epoch, or {@link C#TIME_UNSET} if @@ -101,7 +102,7 @@ public class DashManifest implements FilterableManifestemptyList() : periods; + this.periods = periods == null ? Collections.emptyList() : periods; } public final int getPeriodCount() { @@ -123,10 +124,10 @@ public class DashManifest implements FilterableManifest streamKeys) { - LinkedList keys = new LinkedList<>(streamKeys); + public final DashManifest copy(List streamKeys) { + LinkedList keys = new LinkedList<>(streamKeys); Collections.sort(keys); - keys.add(new RepresentationKey(-1, -1, -1)); // Add a stopper key to the end + keys.add(new StreamKey(-1, -1, -1)); // Add a stopper key to the end ArrayList copyPeriods = new ArrayList<>(); long shiftMs = 0; @@ -153,21 +154,21 @@ public class DashManifest implements FilterableManifest copyAdaptationSets( - List adaptationSets, LinkedList keys) { - RepresentationKey key = keys.poll(); + List adaptationSets, LinkedList keys) { + StreamKey key = keys.poll(); int periodIndex = key.periodIndex; ArrayList copyAdaptationSets = new ArrayList<>(); do { - int adaptationSetIndex = key.adaptationSetIndex; + int adaptationSetIndex = key.groupIndex; AdaptationSet adaptationSet = adaptationSets.get(adaptationSetIndex); List representations = adaptationSet.representations; ArrayList copyRepresentations = new ArrayList<>(); do { - Representation representation = representations.get(key.representationIndex); + Representation representation = representations.get(key.trackIndex); copyRepresentations.add(representation); key = keys.poll(); - } while(key.periodIndex == periodIndex && key.adaptationSetIndex == adaptationSetIndex); + } while (key.periodIndex == periodIndex && key.groupIndex == adaptationSetIndex); copyAdaptationSets.add(new AdaptationSet(adaptationSet.id, adaptationSet.type, copyRepresentations, adaptationSet.accessibilityDescriptors, diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 0a4274e674..5de9773a10 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -247,6 +247,7 @@ public class DashManifestParser extends DefaultHandler int audioChannels = Format.NO_VALUE; int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE); String language = xpp.getAttributeValue(null, "lang"); + String label = xpp.getAttributeValue(null, "label"); String drmSchemeType = null; ArrayList drmSchemeDatas = new ArrayList<>(); ArrayList inbandEventStreams = new ArrayList<>(); @@ -283,9 +284,22 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); } else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) { - RepresentationInfo representationInfo = parseRepresentation(xpp, baseUrl, mimeType, codecs, - width, height, frameRate, audioChannels, audioSamplingRate, language, - selectionFlags, accessibilityDescriptors, segmentBase); + RepresentationInfo representationInfo = + parseRepresentation( + xpp, + baseUrl, + label, + mimeType, + codecs, + width, + height, + frameRate, + audioChannels, + audioSamplingRate, + language, + selectionFlags, + accessibilityDescriptors, + segmentBase); contentType = checkContentTypeConsistency(contentType, getContentType(representationInfo.format)); representationInfos.add(representationInfo); @@ -355,6 +369,7 @@ public class DashManifestParser extends DefaultHandler protected Pair parseContentProtection(XmlPullParser xpp) throws XmlPullParserException, IOException { String schemeType = null; + String licenseServerUrl = null; byte[] data = null; UUID uuid = null; boolean requiresSecureDecoder = false; @@ -364,7 +379,7 @@ public class DashManifestParser extends DefaultHandler switch (Util.toLowerInvariant(schemeIdUri)) { case "urn:mpeg:dash:mp4protection:2011": schemeType = xpp.getAttributeValue(null, "value"); - String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); + String defaultKid = XmlPullParserUtil.getAttributeValueIgnorePrefix(xpp, "default_KID"); if (!TextUtils.isEmpty(defaultKid) && !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) { String[] defaultKidStrings = defaultKid.split("\\s+"); @@ -389,11 +404,14 @@ public class DashManifestParser extends DefaultHandler do { xpp.next(); - if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { + if (XmlPullParserUtil.isStartTag(xpp, "ms:laurl")) { + licenseServerUrl = xpp.getAttributeValue(null, "licenseUrl"); + } else if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { String robustnessLevel = xpp.getAttributeValue(null, "robustness_level"); requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); } else if (data == null) { - if (XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) { + if (XmlPullParserUtil.isStartTagIgnorePrefix(xpp, "pssh") + && xpp.next() == XmlPullParser.TEXT) { // The cenc:pssh element is defined in 23001-7:2015. data = Base64.decode(xpp.getText(), Base64.DEFAULT); uuid = PsshAtomUtil.parseUuid(data); @@ -409,8 +427,11 @@ public class DashManifestParser extends DefaultHandler } } } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); - SchemeData schemeData = uuid != null - ? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) : null; + SchemeData schemeData = + uuid != null + ? new SchemeData( + uuid, licenseServerUrl, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) + : null; return Pair.create(schemeType, schemeData); } @@ -446,12 +467,21 @@ public class DashManifestParser extends DefaultHandler // Representation parsing. - protected RepresentationInfo parseRepresentation(XmlPullParser xpp, String baseUrl, - String adaptationSetMimeType, String adaptationSetCodecs, int adaptationSetWidth, - int adaptationSetHeight, float adaptationSetFrameRate, int adaptationSetAudioChannels, - int adaptationSetAudioSamplingRate, String adaptationSetLanguage, + protected RepresentationInfo parseRepresentation( + XmlPullParser xpp, + String baseUrl, + String label, + String adaptationSetMimeType, + String adaptationSetCodecs, + int adaptationSetWidth, + int adaptationSetHeight, + float adaptationSetFrameRate, + int adaptationSetAudioChannels, + int adaptationSetAudioSamplingRate, + String adaptationSetLanguage, @C.SelectionFlags int adaptationSetSelectionFlags, - List adaptationSetAccessibilityDescriptors, SegmentBase segmentBase) + List adaptationSetAccessibilityDescriptors, + SegmentBase segmentBase) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE); @@ -499,30 +529,74 @@ public class DashManifestParser extends DefaultHandler } } while (!XmlPullParserUtil.isEndTag(xpp, "Representation")); - Format format = buildFormat(id, mimeType, width, height, frameRate, audioChannels, - audioSamplingRate, bandwidth, adaptationSetLanguage, adaptationSetSelectionFlags, - adaptationSetAccessibilityDescriptors, codecs, supplementalProperties); + Format format = + buildFormat( + id, + label, + mimeType, + width, + height, + frameRate, + audioChannels, + audioSamplingRate, + bandwidth, + adaptationSetLanguage, + adaptationSetSelectionFlags, + adaptationSetAccessibilityDescriptors, + codecs, + supplementalProperties); segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(); return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeType, drmSchemeDatas, inbandEventStreams, Representation.REVISION_ID_DEFAULT); } - protected Format buildFormat(String id, String containerMimeType, int width, int height, - float frameRate, int audioChannels, int audioSamplingRate, int bitrate, String language, - @C.SelectionFlags int selectionFlags, List accessibilityDescriptors, - String codecs, List supplementalProperties) { + protected Format buildFormat( + String id, + String label, + String containerMimeType, + int width, + int height, + float frameRate, + int audioChannels, + int audioSamplingRate, + int bitrate, + String language, + @C.SelectionFlags int selectionFlags, + List accessibilityDescriptors, + String codecs, + List supplementalProperties) { String sampleMimeType = getSampleMimeType(containerMimeType, codecs); if (sampleMimeType != null) { if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType)) { sampleMimeType = parseEac3SupplementalProperties(supplementalProperties); } if (MimeTypes.isVideo(sampleMimeType)) { - return Format.createVideoContainerFormat(id, containerMimeType, sampleMimeType, codecs, - bitrate, width, height, frameRate, null, selectionFlags); + return Format.createVideoContainerFormat( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + width, + height, + frameRate, + /* initializationData= */ null, + selectionFlags); } else if (MimeTypes.isAudio(sampleMimeType)) { - return Format.createAudioContainerFormat(id, containerMimeType, sampleMimeType, codecs, - bitrate, audioChannels, audioSamplingRate, null, selectionFlags, language); + return Format.createAudioContainerFormat( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + audioChannels, + audioSamplingRate, + /* initializationData= */ null, + selectionFlags, + language); } else if (mimeTypeIsRawText(sampleMimeType)) { int accessibilityChannel; if (MimeTypes.APPLICATION_CEA608.equals(sampleMimeType)) { @@ -532,12 +606,20 @@ public class DashManifestParser extends DefaultHandler } else { accessibilityChannel = Format.NO_VALUE; } - return Format.createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs, - bitrate, selectionFlags, language, accessibilityChannel); + return Format.createTextContainerFormat( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + selectionFlags, + language, + accessibilityChannel); } } - return Format.createContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate, - selectionFlags, language); + return Format.createContainerFormat( + id, label, containerMimeType, sampleMimeType, codecs, bitrate, selectionFlags, language); } protected Representation buildRepresentation(RepresentationInfo representationInfo, @@ -940,10 +1022,12 @@ public class DashManifestParser extends DefaultHandler } else if (mimeTypeIsRawText(containerMimeType)) { return containerMimeType; } else if (MimeTypes.APPLICATION_MP4.equals(containerMimeType)) { - if ("stpp".equals(codecs)) { - return MimeTypes.APPLICATION_TTML; - } else if ("wvtt".equals(codecs)) { - return MimeTypes.APPLICATION_MP4VTT; + if (codecs != null) { + if (codecs.startsWith("stpp")) { + return MimeTypes.APPLICATION_TTML; + } else if (codecs.startsWith("wvtt")) { + return MimeTypes.APPLICATION_MP4VTT; + } } } else if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { if (codecs != null) { diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java index bb1dbdac5d..b6f7ef0a3b 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java @@ -51,7 +51,7 @@ public class Period { * @param adaptationSets The adaptation sets belonging to the period. */ public Period(@Nullable String id, long startMs, List adaptationSets) { - this(id, startMs, adaptationSets, Collections.emptyList()); + this(id, startMs, adaptationSets, Collections.emptyList()); } /** diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java index 71a3a0122c..44daa1d016 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java @@ -135,8 +135,10 @@ public abstract class Representation { this.revisionId = revisionId; this.format = format; this.baseUrl = baseUrl; - this.inbandEventStreams = inbandEventStreams == null ? Collections.emptyList() - : Collections.unmodifiableList(inbandEventStreams); + this.inbandEventStreams = + inbandEventStreams == null + ? Collections.emptyList() + : Collections.unmodifiableList(inbandEventStreams); initializationUri = segmentBase.getInitialization(this); presentationTimeOffsetUs = segmentBase.getPresentationTimeOffsetUs(); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java deleted file mode 100644 index fd9488af55..0000000000 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.source.dash.manifest; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -/** Uniquely identifies a {@link Representation} in a {@link DashManifest}. */ -public final class RepresentationKey implements Comparable { - - public final int periodIndex; - public final int adaptationSetIndex; - public final int representationIndex; - - public RepresentationKey(int periodIndex, int adaptationSetIndex, int representationIndex) { - this.periodIndex = periodIndex; - this.adaptationSetIndex = adaptationSetIndex; - this.representationIndex = representationIndex; - } - - @Override - public String toString() { - return periodIndex + "." + adaptationSetIndex + "." + representationIndex; - } - - @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - RepresentationKey that = (RepresentationKey) o; - return periodIndex == that.periodIndex - && adaptationSetIndex == that.adaptationSetIndex - && representationIndex == that.representationIndex; - } - - @Override - public int hashCode() { - int result = periodIndex; - result = 31 * result + adaptationSetIndex; - result = 31 * result + representationIndex; - return result; - } - - // Comparable implementation. - - @Override - public int compareTo(@NonNull RepresentationKey o) { - int result = periodIndex - o.periodIndex; - if (result == 0) { - result = adaptationSetIndex - o.adaptationSetIndex; - if (result == 0) { - result = representationIndex - o.representationIndex; - } - } - return result; - } - -} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadAction.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadAction.java index c2facd9626..f36a018e5b 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadAction.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadAction.java @@ -20,55 +20,65 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.SegmentDownloadAction; -import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +import com.google.android.exoplayer2.offline.StreamKey; +import java.util.Collections; import java.util.List; /** An action to download or remove downloaded DASH streams. */ -public final class DashDownloadAction extends SegmentDownloadAction { +public final class DashDownloadAction extends SegmentDownloadAction { private static final String TYPE = "dash"; private static final int VERSION = 0; public static final Deserializer DESERIALIZER = - new SegmentDownloadActionDeserializer(TYPE, VERSION) { - - @Override - protected RepresentationKey readKey(DataInputStream input) throws IOException { - return new RepresentationKey(input.readInt(), input.readInt(), input.readInt()); - } - + new SegmentDownloadActionDeserializer(TYPE, VERSION) { @Override protected DownloadAction createDownloadAction( - Uri uri, boolean isRemoveAction, byte[] data, List keys) { + Uri uri, boolean isRemoveAction, byte[] data, List keys) { return new DashDownloadAction(uri, isRemoveAction, data, keys); } }; + /** + * Creates a DASH download action. + * + * @param uri The URI of the media to be downloaded. + * @param data Optional custom data for this action. If {@code null} an empty array will be used. + * @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded. + */ + public static DashDownloadAction createDownloadAction( + Uri uri, @Nullable byte[] data, List keys) { + return new DashDownloadAction(uri, /* isRemoveAction= */ false, data, keys); + } + + /** + * Creates a DASH remove action. + * + * @param uri The URI of the media to be removed. + * @param data Optional custom data for this action. If {@code null} an empty array will be used. + */ + public static DashDownloadAction createRemoveAction(Uri uri, @Nullable byte[] data) { + return new DashDownloadAction(uri, /* isRemoveAction= */ true, data, Collections.emptyList()); + } + /** * @param uri The DASH manifest URI. * @param isRemoveAction Whether the data will be removed. If {@code false} it will be downloaded. * @param data Optional custom data for this action. * @param keys Keys of representations to be downloaded. If empty, all representations are * downloaded. If {@code removeAction} is true, {@code keys} must be empty. + * @deprecated Use {@link #createDownloadAction(Uri, byte[], List)} or {@link + * #createRemoveAction(Uri, byte[])}. */ + @Deprecated public DashDownloadAction( - Uri uri, boolean isRemoveAction, @Nullable byte[] data, List keys) { + Uri uri, boolean isRemoveAction, @Nullable byte[] data, List keys) { super(TYPE, VERSION, uri, isRemoveAction, data, keys); } @Override - protected DashDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { + public DashDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { return new DashDownloader(uri, keys, constructorHelper); } - @Override - protected void writeKey(DataOutputStream output, RepresentationKey key) throws IOException { - output.writeInt(key.periodIndex); - output.writeInt(key.adaptationSetIndex); - output.writeInt(key.representationIndex); - } - } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java index bd19ff6587..91e41b9ded 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java @@ -17,8 +17,10 @@ package com.google.android.exoplayer2.source.dash.offline; import android.net.Uri; import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.TrackKey; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -26,13 +28,11 @@ import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.Representation; -import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -51,9 +51,9 @@ public final class DashDownloadHelper extends DownloadHelper { @Override protected void prepareInternal() throws IOException { + DataSource dataSource = manifestDataSourceFactory.createDataSource(); manifest = - ParsingLoadable.load( - manifestDataSourceFactory.createDataSource(), new DashManifestParser(), uri); + ParsingLoadable.load(dataSource, new DashManifestParser(), uri, C.DATA_TYPE_MANIFEST); } /** Returns the DASH manifest. Must not be called until after preparation completes. */ @@ -87,23 +87,20 @@ public final class DashDownloadHelper extends DownloadHelper { @Override public DashDownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { - return new DashDownloadAction( - uri, /* isRemoveAction= */ false, data, toRepresentationKeys(trackKeys)); + return DashDownloadAction.createDownloadAction(uri, data, toStreamKeys(trackKeys)); } @Override public DashDownloadAction getRemoveAction(@Nullable byte[] data) { - return new DashDownloadAction( - uri, /* isRemoveAction= */ true, data, Collections.emptyList()); + return DashDownloadAction.createRemoveAction(uri, data); } - private static List toRepresentationKeys(List trackKeys) { - List representationKeys = new ArrayList<>(trackKeys.size()); + private static List toStreamKeys(List trackKeys) { + List streamKeys = new ArrayList<>(trackKeys.size()); for (int i = 0; i < trackKeys.size(); i++) { TrackKey trackKey = trackKeys.get(i); - representationKeys.add( - new RepresentationKey(trackKey.periodIndex, trackKey.groupIndex, trackKey.trackIndex)); + streamKeys.add(new StreamKey(trackKey.periodIndex, trackKey.groupIndex, trackKey.trackIndex)); } - return representationKeys; + return streamKeys; } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java index 6922e56b84..68120d6177 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.offline.DownloadException; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.SegmentDownloader; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.dash.DashSegmentIndex; import com.google.android.exoplayer2.source.dash.DashUtil; import com.google.android.exoplayer2.source.dash.DashWrappingSegmentIndex; @@ -30,7 +31,6 @@ import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.Period; import com.google.android.exoplayer2.source.dash.manifest.RangedUri; import com.google.android.exoplayer2.source.dash.manifest.Representation; -import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import java.io.IOException; @@ -51,9 +51,7 @@ import java.util.List; * // period. * DashDownloader dashDownloader = * new DashDownloader( - * manifestUrl, - * Collections.singletonList(new RepresentationKey(0, 0, 0)), - * constructorHelper); + * manifestUrl, Collections.singletonList(new StreamKey(0, 0, 0)), constructorHelper); * // Perform the download. * dashDownloader.download(); * // Access downloaded data using CacheDataSource @@ -61,19 +59,17 @@ import java.util.List; * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE); * } */ -public final class DashDownloader extends SegmentDownloader { +public final class DashDownloader extends SegmentDownloader { /** * @param manifestUri The {@link Uri} of the manifest to be downloaded. - * @param representationKeys Keys defining which representations in the manifest should be - * selected for download. If empty, all representations are downloaded. + * @param streamKeys Keys defining which representations in the manifest should be selected for + * download. If empty, all representations are downloaded. * @param constructorHelper A {@link DownloaderConstructorHelper} instance. */ public DashDownloader( - Uri manifestUri, - List representationKeys, - DownloaderConstructorHelper constructorHelper) { - super(manifestUri, representationKeys, constructorHelper); + Uri manifestUri, List streamKeys, DownloaderConstructorHelper constructorHelper) { + super(manifestUri, streamKeys, constructorHelper); } @Override @@ -167,7 +163,9 @@ public final class DashDownloader extends SegmentDownloader keys = + List keys = Arrays.asList( - new RepresentationKey(0, 0, 0), - new RepresentationKey(0, 0, 1), - new RepresentationKey(0, 1, 2), - new RepresentationKey(1, 0, 1), - new RepresentationKey(1, 1, 0), - new RepresentationKey(1, 1, 2), - new RepresentationKey(2, 0, 1), - new RepresentationKey(2, 0, 2), - new RepresentationKey(2, 1, 0)); + new StreamKey(0, 0, 0), + new StreamKey(0, 0, 1), + new StreamKey(0, 1, 2), + new StreamKey(1, 0, 1), + new StreamKey(1, 1, 0), + new StreamKey(1, 1, 2), + new StreamKey(2, 0, 1), + new StreamKey(2, 0, 2), + new StreamKey(2, 1, 0)); // Keys don't need to be in any particular order Collections.shuffle(keys, new Random(0)); @@ -105,8 +106,7 @@ public class DashManifestTest { newPeriod("4", 4, newAdaptationSet(5, representations[1][0]))); DashManifest copyManifest = - sourceManifest.copy( - Arrays.asList(new RepresentationKey(0, 0, 0), new RepresentationKey(1, 0, 0))); + sourceManifest.copy(Arrays.asList(new StreamKey(0, 0, 0), new StreamKey(1, 0, 0))); DashManifest expectedManifest = newDashManifest( @@ -141,12 +141,12 @@ public class DashManifestTest { DashManifest copyManifest = sourceManifest.copy( Arrays.asList( - new RepresentationKey(0, 0, 0), - new RepresentationKey(0, 0, 1), - new RepresentationKey(0, 1, 2), - new RepresentationKey(2, 0, 1), - new RepresentationKey(2, 0, 2), - new RepresentationKey(2, 1, 0))); + new StreamKey(0, 0, 0), + new StreamKey(0, 0, 1), + new StreamKey(0, 1, 2), + new StreamKey(2, 0, 1), + new StreamKey(2, 0, 2), + new StreamKey(2, 1, 0))); DashManifest expectedManifest = newDashManifest( diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java index 309e6c8eb0..12c0b9239e 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java @@ -32,37 +32,30 @@ public class RepresentationTest { public void testGetCacheKey() { String uri = "http://www.google.com"; SegmentBase base = new SingleSegmentBase(new RangedUri(null, 0, 1), 1, 0, 1, 1); - Format format = - Format.createVideoContainerFormat( - "0", - MimeTypes.APPLICATION_MP4, - null, - MimeTypes.VIDEO_H264, - 2500000, - 1920, - 1080, - Format.NO_VALUE, - null, - 0); + Format format = createVideoContainerFormat("0"); Representation representation = Representation.newInstance("test_stream_1", 3, format, uri, base); assertThat(representation.getCacheKey()).isEqualTo("test_stream_1.0.3"); - format = - Format.createVideoContainerFormat( - "150", - MimeTypes.APPLICATION_MP4, - null, - MimeTypes.VIDEO_H264, - 2500000, - 1920, - 1080, - Format.NO_VALUE, - null, - 0); + format = createVideoContainerFormat("150"); representation = Representation.newInstance( "test_stream_1", Representation.REVISION_ID_DEFAULT, format, uri, base); assertThat(representation.getCacheKey()).isEqualTo("test_stream_1.150.-1"); } + + private static Format createVideoContainerFormat(String id) { + return Format.createVideoContainerFormat( + id, + "label", + /* containerMimeType= */ MimeTypes.APPLICATION_MP4, + /* sampleMimeType= */ MimeTypes.VIDEO_H264, + /* codecs= */ null, + /* bitrate= */ 2500000, + /* width= */ 1920, + /* height= */ 1080, + /* frameRate= */ Format.NO_VALUE, + /* initializationData= */ null, + /* selectionFlags= */ 0); + } } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadActionTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadActionTest.java index 43d9bd9965..0ebf6bb628 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadActionTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadActionTest.java @@ -18,10 +18,9 @@ package com.google.android.exoplayer2.source.dash.offline; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; -import android.support.annotation.Nullable; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; import java.io.ByteArrayInputStream; @@ -38,9 +37,7 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -/** - * Unit tests for {@link DashDownloadAction}. - */ +/** Unit tests for {@link DashDownloadAction}. */ @RunWith(RobolectricTestRunner.class) public class DashDownloadActionTest { @@ -55,134 +52,106 @@ public class DashDownloadActionTest { @Test public void testDownloadActionIsNotRemoveAction() { - DashDownloadAction action = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); + DownloadAction action = createDownloadAction(uri1); assertThat(action.isRemoveAction).isFalse(); } @Test - public void testRemoveActionisRemoveAction() { - DashDownloadAction action2 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); + public void testRemoveActionIsRemoveAction() { + DownloadAction action2 = createRemoveAction(uri1); assertThat(action2.isRemoveAction).isTrue(); } @Test public void testCreateDownloader() { MockitoAnnotations.initMocks(this); - DashDownloadAction action = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); - DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper( - Mockito.mock(Cache.class), DummyDataSource.FACTORY); + DownloadAction action = createDownloadAction(uri1); + DownloaderConstructorHelper constructorHelper = + new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); assertThat(action.createDownloader(constructorHelper)).isNotNull(); } @Test public void testSameUriDifferentAction_IsSameMedia() { - DashDownloadAction action1 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); - DashDownloadAction action2 = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); + DownloadAction action1 = createRemoveAction(uri1); + DownloadAction action2 = createDownloadAction(uri1); assertThat(action1.isSameMedia(action2)).isTrue(); } @Test public void testDifferentUriAndAction_IsNotSameMedia() { - DashDownloadAction action3 = newAction(uri2, /* isRemoveAction= */ true, /* data= */ null); - DashDownloadAction action4 = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); + DownloadAction action3 = createRemoveAction(uri2); + DownloadAction action4 = createDownloadAction(uri1); assertThat(action3.isSameMedia(action4)).isFalse(); } @SuppressWarnings("EqualsWithItself") @Test public void testEquals() { - DashDownloadAction action1 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); + DownloadAction action1 = createRemoveAction(uri1); assertThat(action1.equals(action1)).isTrue(); - DashDownloadAction action2 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); - DashDownloadAction action3 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); + DownloadAction action2 = createRemoveAction(uri1); + DownloadAction action3 = createRemoveAction(uri1); assertEqual(action2, action3); - DashDownloadAction action4 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); - DashDownloadAction action5 = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); + DownloadAction action4 = createRemoveAction(uri1); + DownloadAction action5 = createDownloadAction(uri1); assertNotEqual(action4, action5); - DashDownloadAction action6 = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); - DashDownloadAction action7 = - newAction( - uri1, /* isRemoveAction= */ false, /* data= */ null, new RepresentationKey(0, 0, 0)); + DownloadAction action6 = createDownloadAction(uri1); + DownloadAction action7 = createDownloadAction(uri1, new StreamKey(0, 0, 0)); assertNotEqual(action6, action7); - DashDownloadAction action8 = - newAction( - uri1, /* isRemoveAction= */ false, /* data= */ null, new RepresentationKey(1, 1, 1)); - DashDownloadAction action9 = - newAction( - uri1, /* isRemoveAction= */ false, /* data= */ null, new RepresentationKey(0, 0, 0)); + DownloadAction action8 = createDownloadAction(uri1, new StreamKey(1, 1, 1)); + DownloadAction action9 = createDownloadAction(uri1, new StreamKey(0, 0, 0)); assertNotEqual(action8, action9); - DashDownloadAction action10 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); - DashDownloadAction action11 = newAction(uri2, /* isRemoveAction= */ true, /* data= */ null); + DownloadAction action10 = createRemoveAction(uri1); + DownloadAction action11 = createRemoveAction(uri2); assertNotEqual(action10, action11); - DashDownloadAction action12 = - newAction( - uri1, - /* isRemoveAction= */ false, - /* data= */ null, - new RepresentationKey(0, 0, 0), - new RepresentationKey(1, 1, 1)); - DashDownloadAction action13 = - newAction( - uri1, - /* isRemoveAction= */ false, - /* data= */ null, - new RepresentationKey(1, 1, 1), - new RepresentationKey(0, 0, 0)); + DownloadAction action12 = + createDownloadAction(uri1, new StreamKey(0, 0, 0), new StreamKey(1, 1, 1)); + DownloadAction action13 = + createDownloadAction(uri1, new StreamKey(1, 1, 1), new StreamKey(0, 0, 0)); assertEqual(action12, action13); - DashDownloadAction action14 = - newAction( - uri1, /* isRemoveAction= */ false, /* data= */ null, new RepresentationKey(0, 0, 0)); - DashDownloadAction action15 = - newAction( - uri1, - /* isRemoveAction= */ false, - /* data= */ null, - new RepresentationKey(1, 1, 1), - new RepresentationKey(0, 0, 0)); + DownloadAction action14 = createDownloadAction(uri1, new StreamKey(0, 0, 0)); + DownloadAction action15 = + createDownloadAction(uri1, new StreamKey(1, 1, 1), new StreamKey(0, 0, 0)); assertNotEqual(action14, action15); - DashDownloadAction action16 = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); - DashDownloadAction action17 = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); + DownloadAction action16 = createDownloadAction(uri1); + DownloadAction action17 = createDownloadAction(uri1); assertEqual(action16, action17); } @Test public void testSerializerGetType() { - DashDownloadAction action = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); + DownloadAction action = createDownloadAction(uri1); assertThat(action.type).isNotNull(); } @Test public void testSerializerWriteRead() throws Exception { - doTestSerializationRoundTrip(newAction(uri1, /* isRemoveAction= */ false, /* data= */ null)); - doTestSerializationRoundTrip(newAction(uri1, /* isRemoveAction= */ true, /* data= */ null)); + doTestSerializationRoundTrip(createDownloadAction(uri1)); + doTestSerializationRoundTrip(createRemoveAction(uri1)); doTestSerializationRoundTrip( - newAction( - uri2, - /* isRemoveAction= */ false, - /* data= */ null, - new RepresentationKey(0, 0, 0), - new RepresentationKey(1, 1, 1))); + createDownloadAction(uri2, new StreamKey(0, 0, 0), new StreamKey(1, 1, 1))); } - private static void assertNotEqual(DashDownloadAction action1, DashDownloadAction action2) { + private static void assertNotEqual(DownloadAction action1, DownloadAction action2) { assertThat(action1).isNotEqualTo(action2); assertThat(action2).isNotEqualTo(action1); } - private static void assertEqual(DashDownloadAction action1, DashDownloadAction action2) { + private static void assertEqual(DownloadAction action1, DownloadAction action2) { assertThat(action1).isEqualTo(action2); assertThat(action2).isEqualTo(action1); } - private static void doTestSerializationRoundTrip(DashDownloadAction action) throws IOException { + private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(out); DownloadAction.serializeToStream(action, output); @@ -196,10 +165,13 @@ public class DashDownloadActionTest { assertThat(action).isEqualTo(action2); } - private static DashDownloadAction newAction( - Uri uri, boolean isRemoveAction, @Nullable byte[] data, RepresentationKey... keys) { - ArrayList keysList = new ArrayList<>(); + private static DownloadAction createDownloadAction(Uri uri, StreamKey... keys) { + ArrayList keysList = new ArrayList<>(); Collections.addAll(keysList, keys); - return new DashDownloadAction(uri, isRemoveAction, data, keysList); + return DashDownloadAction.createDownloadAction(uri, null, keysList); + } + + private static DownloadAction createRemoveAction(Uri uri) { + return DashDownloadAction.createRemoveAction(uri, null); } } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java index 4c96357528..841da07114 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java @@ -27,7 +27,7 @@ import static org.mockito.Mockito.when; import com.google.android.exoplayer2.offline.DownloadException; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; @@ -77,7 +77,7 @@ public class DashDownloaderTest { .setRandomData("audio_segment_2", 5) .setRandomData("audio_segment_3", 6); - DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new RepresentationKey(0, 0, 0)); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0)); dashDownloader.download(); assertCachedData(cache, fakeDataSet); } @@ -96,7 +96,7 @@ public class DashDownloaderTest { .setRandomData("audio_segment_2", 5) .setRandomData("audio_segment_3", 6); - DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new RepresentationKey(0, 0, 0)); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0)); dashDownloader.download(); assertCachedData(cache, fakeDataSet); } @@ -115,8 +115,7 @@ public class DashDownloaderTest { .setRandomData("text_segment_3", 3); DashDownloader dashDownloader = - getDashDownloader( - fakeDataSet, new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)); + getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0)); dashDownloader.download(); assertCachedData(cache, fakeDataSet); } @@ -159,7 +158,7 @@ public class DashDownloaderTest { when(factory.createDataSource()).thenReturn(fakeDataSource); DashDownloader dashDownloader = - getDashDownloader(factory, new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)); + getDashDownloader(factory, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0)); dashDownloader.download(); DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs(); @@ -191,7 +190,7 @@ public class DashDownloaderTest { when(factory.createDataSource()).thenReturn(fakeDataSource); DashDownloader dashDownloader = - getDashDownloader(factory, new RepresentationKey(0, 0, 0), new RepresentationKey(1, 0, 0)); + getDashDownloader(factory, new StreamKey(0, 0, 0), new StreamKey(1, 0, 0)); dashDownloader.download(); DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs(); @@ -220,7 +219,7 @@ public class DashDownloaderTest { .endData() .setRandomData("audio_segment_3", 6); - DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new RepresentationKey(0, 0, 0)); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0)); try { dashDownloader.download(); fail(); @@ -245,7 +244,7 @@ public class DashDownloaderTest { .endData() .setRandomData("audio_segment_3", 6); - DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new RepresentationKey(0, 0, 0)); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0)); assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(0); try { @@ -274,8 +273,7 @@ public class DashDownloaderTest { .setRandomData("text_segment_3", 3); DashDownloader dashDownloader = - getDashDownloader( - fakeDataSet, new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)); + getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0)); dashDownloader.download(); dashDownloader.remove(); assertCacheEmpty(cache); @@ -288,7 +286,7 @@ public class DashDownloaderTest { .setData(TEST_MPD_URI, TEST_MPD_NO_INDEX) .setRandomData("test_segment_1", 4); - DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new RepresentationKey(0, 0, 0)); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0)); try { dashDownloader.download(); fail(); @@ -299,17 +297,17 @@ public class DashDownloaderTest { assertCacheEmpty(cache); } - private DashDownloader getDashDownloader(FakeDataSet fakeDataSet, RepresentationKey... keys) { + private DashDownloader getDashDownloader(FakeDataSet fakeDataSet, StreamKey... keys) { return getDashDownloader(new Factory(null).setFakeDataSet(fakeDataSet), keys); } - private DashDownloader getDashDownloader(Factory factory, RepresentationKey... keys) { + private DashDownloader getDashDownloader(Factory factory, StreamKey... keys) { return new DashDownloader( TEST_MPD_URI, keysList(keys), new DownloaderConstructorHelper(cache, factory)); } - private static ArrayList keysList(RepresentationKey... keys) { - ArrayList keysList = new ArrayList<>(); + private static ArrayList keysList(StreamKey... keys) { + ArrayList keysList = new ArrayList<>(); Collections.addAll(keysList, keys); return keysList; } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index 8ca2aa083b..d2ba826c66 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -25,9 +25,10 @@ import android.content.Context; import android.net.Uri; import android.os.ConditionVariable; import android.support.annotation.Nullable; +import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; @@ -62,8 +63,8 @@ public class DownloadManagerDashTest { private File tempFolder; private FakeDataSet fakeDataSet; private DownloadManager downloadManager; - private RepresentationKey fakeRepresentationKey1; - private RepresentationKey fakeRepresentationKey2; + private StreamKey fakeStreamKey1; + private StreamKey fakeStreamKey2; private TestDownloadManagerListener downloadManagerListener; private File actionFile; private DummyMainThread dummyMainThread; @@ -88,8 +89,8 @@ public class DownloadManagerDashTest { .setRandomData("text_segment_2", 2) .setRandomData("text_segment_3", 3); - fakeRepresentationKey1 = new RepresentationKey(0, 0, 0); - fakeRepresentationKey2 = new RepresentationKey(0, 1, 0); + fakeStreamKey1 = new StreamKey(0, 0, 0); + fakeStreamKey2 = new StreamKey(0, 1, 0); actionFile = new File(tempFolder, "actionFile"); createDownloadManager(); } @@ -133,7 +134,7 @@ public class DownloadManagerDashTest { @Override public void run() { // Setup an Action and immediately release the DM. - handleDownloadAction(fakeRepresentationKey1, fakeRepresentationKey2); + handleDownloadAction(fakeStreamKey1, fakeStreamKey2); downloadManager.release(); } }); @@ -160,15 +161,15 @@ public class DownloadManagerDashTest { @Test public void testHandleDownloadAction() throws Throwable { - handleDownloadAction(fakeRepresentationKey1, fakeRepresentationKey2); + handleDownloadAction(fakeStreamKey1, fakeStreamKey2); blockUntilTasksCompleteAndThrowAnyDownloadError(); assertCachedData(cache, fakeDataSet); } @Test public void testHandleMultipleDownloadAction() throws Throwable { - handleDownloadAction(fakeRepresentationKey1); - handleDownloadAction(fakeRepresentationKey2); + handleDownloadAction(fakeStreamKey1); + handleDownloadAction(fakeStreamKey2); blockUntilTasksCompleteAndThrowAnyDownloadError(); assertCachedData(cache, fakeDataSet); } @@ -181,13 +182,13 @@ public class DownloadManagerDashTest { new Runnable() { @Override public void run() { - handleDownloadAction(fakeRepresentationKey2); + handleDownloadAction(fakeStreamKey2); } }) .appendReadData(TestUtil.buildTestData(5)) .endData(); - handleDownloadAction(fakeRepresentationKey1); + handleDownloadAction(fakeStreamKey1); blockUntilTasksCompleteAndThrowAnyDownloadError(); assertCachedData(cache, fakeDataSet); @@ -195,7 +196,7 @@ public class DownloadManagerDashTest { @Test public void testHandleRemoveAction() throws Throwable { - handleDownloadAction(fakeRepresentationKey1); + handleDownloadAction(fakeStreamKey1); blockUntilTasksCompleteAndThrowAnyDownloadError(); @@ -210,7 +211,7 @@ public class DownloadManagerDashTest { @Ignore @Test public void testHandleRemoveActionBeforeDownloadFinish() throws Throwable { - handleDownloadAction(fakeRepresentationKey1); + handleDownloadAction(fakeStreamKey1); handleRemoveAction(); blockUntilTasksCompleteAndThrowAnyDownloadError(); @@ -233,7 +234,7 @@ public class DownloadManagerDashTest { .appendReadData(TestUtil.buildTestData(5)) .endData(); - handleDownloadAction(fakeRepresentationKey1); + handleDownloadAction(fakeStreamKey1); assertThat(downloadInProgressCondition.block(ASSERT_TRUE_TIMEOUT)).isTrue(); @@ -248,7 +249,7 @@ public class DownloadManagerDashTest { downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } - private void handleDownloadAction(RepresentationKey... keys) { + private void handleDownloadAction(StreamKey... keys) { downloadManager.handleAction(newAction(TEST_MPD_URI, false, null, keys)); } @@ -279,10 +280,16 @@ public class DownloadManagerDashTest { }); } - private static DashDownloadAction newAction( - Uri uri, boolean isRemoveAction, @Nullable byte[] data, RepresentationKey... keys) { - ArrayList keysList = new ArrayList<>(); + private static DownloadAction newAction( + Uri uri, boolean isRemoveAction, @Nullable byte[] data, StreamKey... keys) { + ArrayList keysList = new ArrayList<>(); Collections.addAll(keysList, keys); - return new DashDownloadAction(uri, isRemoveAction, data, keysList); + DownloadAction result; + if (isRemoveAction) { + result = DashDownloadAction.createRemoveAction(uri, data); + } else { + result = DashDownloadAction.createDownloadAction(uri, data, keysList); + } + return result; } } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java index 745acd9bbf..c0f48857c2 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java @@ -20,18 +20,16 @@ import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTest import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty; import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; -import android.app.Notification; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.support.annotation.Nullable; +import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadManager; -import com.google.android.exoplayer2.offline.DownloadManager.TaskState; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.scheduler.Requirements; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.scheduler.Scheduler; -import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; @@ -52,7 +50,6 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mockito; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -65,8 +62,8 @@ public class DownloadServiceDashTest { private SimpleCache cache; private File tempFolder; private FakeDataSet fakeDataSet; - private RepresentationKey fakeRepresentationKey1; - private RepresentationKey fakeRepresentationKey2; + private StreamKey fakeStreamKey1; + private StreamKey fakeStreamKey2; private Context context; private DownloadService dashDownloadService; private ConditionVariable pauseDownloadCondition; @@ -108,8 +105,8 @@ public class DownloadServiceDashTest { .setRandomData("text_segment_3", 3); final DataSource.Factory fakeDataSourceFactory = new FakeDataSource.Factory(null).setFakeDataSet(fakeDataSet); - fakeRepresentationKey1 = new RepresentationKey(0, 0, 0); - fakeRepresentationKey2 = new RepresentationKey(0, 1, 0); + fakeStreamKey1 = new StreamKey(0, 0, 0); + fakeStreamKey2 = new StreamKey(0, 1, 0); dummyMainThread.runOnMainThread( new Runnable() { @@ -135,29 +132,17 @@ public class DownloadServiceDashTest { dashDownloadManager.startDownloads(); dashDownloadService = - new DownloadService(/*foregroundNotificationId=*/ 1) { - + new DownloadService(DownloadService.FOREGROUND_NOTIFICATION_ID_NONE) { @Override protected DownloadManager getDownloadManager() { return dashDownloadManager; } - @Override - protected Notification getForegroundNotification(TaskState[] taskStates) { - return Mockito.mock(Notification.class); - } - @Nullable @Override protected Scheduler getScheduler() { return null; } - - @Nullable - @Override - protected Requirements getRequirements() { - return null; - } }; dashDownloadService.onCreate(); } @@ -180,8 +165,8 @@ public class DownloadServiceDashTest { @Ignore // b/78877092 @Test public void testMultipleDownloadAction() throws Throwable { - downloadKeys(fakeRepresentationKey1); - downloadKeys(fakeRepresentationKey2); + downloadKeys(fakeStreamKey1); + downloadKeys(fakeStreamKey2); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); @@ -191,7 +176,7 @@ public class DownloadServiceDashTest { @Ignore // b/78877092 @Test public void testRemoveAction() throws Throwable { - downloadKeys(fakeRepresentationKey1, fakeRepresentationKey2); + downloadKeys(fakeStreamKey1, fakeStreamKey2); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); @@ -206,7 +191,7 @@ public class DownloadServiceDashTest { @Test public void testRemoveBeforeDownloadComplete() throws Throwable { pauseDownloadCondition = new ConditionVariable(); - downloadKeys(fakeRepresentationKey1, fakeRepresentationKey2); + downloadKeys(fakeStreamKey1, fakeStreamKey2); removeAll(); @@ -219,11 +204,11 @@ public class DownloadServiceDashTest { callDownloadServiceOnStart(newAction(TEST_MPD_URI, true, null)); } - private void downloadKeys(RepresentationKey... keys) { + private void downloadKeys(StreamKey... keys) { callDownloadServiceOnStart(newAction(TEST_MPD_URI, false, null, keys)); } - private void callDownloadServiceOnStart(final DashDownloadAction action) { + private void callDownloadServiceOnStart(final DownloadAction action) { dummyMainThread.runOnMainThread( new Runnable() { @Override @@ -235,10 +220,16 @@ public class DownloadServiceDashTest { }); } - private static DashDownloadAction newAction( - Uri uri, boolean isRemoveAction, @Nullable byte[] data, RepresentationKey... keys) { - ArrayList keysList = new ArrayList<>(); + private static DownloadAction newAction( + Uri uri, boolean isRemoveAction, @Nullable byte[] data, StreamKey... keys) { + ArrayList keysList = new ArrayList<>(); Collections.addAll(keysList, keys); - return new DashDownloadAction(uri, isRemoveAction, data, keysList); + DownloadAction result; + if (isRemoveAction) { + result = DashDownloadAction.createRemoveAction(uri, data); + } else { + result = DashDownloadAction.createDownloadAction(uri, data, keysList); + } + return result; } } diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 6aeb33e195..af02544619 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -18,9 +18,15 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion + consumerProguardFiles 'proguard-rules.txt' } buildTypes { diff --git a/library/hls/proguard-rules.txt b/library/hls/proguard-rules.txt new file mode 100644 index 0000000000..3b8d1bb4ac --- /dev/null +++ b/library/hls/proguard-rules.txt @@ -0,0 +1,7 @@ +# Proguard rules specific to the hls module. + +# Constructors accessed via reflection in SegmentDownloadAction +-dontnote com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction +-keepclassmembers class com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction { + static ** DESERIALIZER; +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java index 3f57cba1b0..55a648a0b8 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java @@ -16,10 +16,12 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; @@ -27,6 +29,8 @@ import java.security.InvalidKeyException; import java.security.Key; import java.security.NoSuchAlgorithmException; import java.security.spec.AlgorithmParameterSpec; +import java.util.List; +import java.util.Map; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.NoSuchPaddingException; @@ -36,18 +40,18 @@ import javax.crypto.spec.SecretKeySpec; /** * A {@link DataSource} that decrypts data read from an upstream source, encrypted with AES-128 with * a 128-bit key and PKCS7 padding. - *

    - * Note that this {@link DataSource} does not support being opened from arbitrary offsets. It is + * + *

    Note that this {@link DataSource} does not support being opened from arbitrary offsets. It is * designed specifically for reading whole files as defined in an HLS media playlist. For this * reason the implementation is private to the HLS package. */ -/* package */ final class Aes128DataSource implements DataSource { +/* package */ class Aes128DataSource implements DataSource { private final DataSource upstream; private final byte[] encryptionKey; private final byte[] encryptionIv; - private CipherInputStream cipherInputStream; + private @Nullable CipherInputStream cipherInputStream; /** * @param upstream The upstream {@link DataSource}. @@ -61,10 +65,15 @@ import javax.crypto.spec.SecretKeySpec; } @Override - public long open(DataSpec dataSpec) throws IOException { + public final void addTransferListener(TransferListener transferListener) { + upstream.addTransferListener(transferListener); + } + + @Override + public final long open(DataSpec dataSpec) throws IOException { Cipher cipher; try { - cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); + cipher = getCipherInstance(); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { throw new RuntimeException(e); } @@ -86,16 +95,8 @@ import javax.crypto.spec.SecretKeySpec; } @Override - public void close() throws IOException { - if (cipherInputStream != null) { - cipherInputStream = null; - upstream.close(); - } - } - - @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { - Assertions.checkState(cipherInputStream != null); + public final int read(byte[] buffer, int offset, int readLength) throws IOException { + Assertions.checkNotNull(cipherInputStream); int bytesRead = cipherInputStream.read(buffer, offset, readLength); if (bytesRead < 0) { return C.RESULT_END_OF_INPUT; @@ -104,8 +105,24 @@ import javax.crypto.spec.SecretKeySpec; } @Override - public Uri getUri() { + public final @Nullable Uri getUri() { return upstream.getUri(); } + @Override + public final Map> getResponseHeaders() { + return upstream.getResponseHeaders(); + } + + @Override + public void close() throws IOException { + if (cipherInputStream != null) { + cipherInputStream = null; + upstream.close(); + } + } + + protected Cipher getCipherInstance() throws NoSuchPaddingException, NoSuchAlgorithmException { + return Cipher.getInstance("AES/CBC/PKCS7Padding"); + } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java index 702b1126cc..35c71fc86d 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TimestampAdjuster; import java.util.Collections; import java.util.List; +import java.util.Map; /** * Default {@link HlsExtractorFactory} implementation. @@ -48,9 +49,14 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { public static final String WEBVTT_FILE_EXTENSION = ".webvtt"; @Override - public Pair createExtractor(Extractor previousExtractor, Uri uri, - Format format, List muxedCaptionFormats, DrmInitData drmInitData, - TimestampAdjuster timestampAdjuster) { + public Pair createExtractor( + Extractor previousExtractor, + Uri uri, + Format format, + List muxedCaptionFormats, + DrmInitData drmInitData, + TimestampAdjuster timestampAdjuster, + Map> responseHeaders) { String lastPathSegment = uri.getLastPathSegment(); if (lastPathSegment == null) { lastPathSegment = ""; @@ -77,8 +83,13 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4) || lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) { - extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData, - muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList()); + extractor = + new FragmentedMp4Extractor( + /* flags= */ 0, + timestampAdjuster, + /* sideloadedTrack= */ null, + drmInitData, + muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList()); } else { // For any other file extension, we assume TS format. @DefaultTsPayloadReaderFactory.Flags @@ -87,7 +98,15 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { // The playlist declares closed caption renditions, we should ignore descriptors. esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS; } else { - muxedCaptionFormats = Collections.emptyList(); + // The playlist does not provide any closed caption information. We preemptively declare a + // closed caption track on channel 0. + muxedCaptionFormats = + Collections.singletonList( + Format.createTextSampleFormat( + /* id= */ null, + MimeTypes.APPLICATION_CEA608, + /* selectionFlags= */ 0, + /* language= */ null)); } String codecs = format.codecs; if (!TextUtils.isEmpty(codecs)) { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 0fb1b6a969..ae50c93b83 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -17,12 +17,12 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.Chunk; -import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; import com.google.android.exoplayer2.source.chunk.DataChunk; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.trackselection.BaseTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.Util; @@ -104,7 +105,7 @@ import java.util.List; // the way in which HlsSampleStreamWrapper generates track groups. Use only index based methods // in TrackSelection to avoid unexpected behavior. private TrackSelection trackSelection; - private long liveEdgeTimeUs; + private long liveEdgeInPeriodTimeUs; private boolean seenExpectedPlaylistError; /** @@ -114,21 +115,28 @@ import java.util.List; * @param variants The available variants. * @param dataSourceFactory An {@link HlsDataSourceFactory} to create {@link DataSource}s for the * chunks. - * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If - * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the - * same provider. + * @param mediaTransferListener The transfer listener which should be informed of any media data + * transfers. May be null if no listener is available. + * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If multiple + * {@link HlsChunkSource}s are used for a single playback, they should all share the same + * provider. * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption * information is available in the master playlist. */ - public HlsChunkSource(HlsExtractorFactory extractorFactory, HlsPlaylistTracker playlistTracker, - HlsUrl[] variants, HlsDataSourceFactory dataSourceFactory, - TimestampAdjusterProvider timestampAdjusterProvider, List muxedCaptionFormats) { + public HlsChunkSource( + HlsExtractorFactory extractorFactory, + HlsPlaylistTracker playlistTracker, + HlsUrl[] variants, + HlsDataSourceFactory dataSourceFactory, + @Nullable TransferListener mediaTransferListener, + TimestampAdjusterProvider timestampAdjusterProvider, + List muxedCaptionFormats) { this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; this.variants = variants; this.timestampAdjusterProvider = timestampAdjusterProvider; this.muxedCaptionFormats = muxedCaptionFormats; - liveEdgeTimeUs = C.TIME_UNSET; + liveEdgeInPeriodTimeUs = C.TIME_UNSET; Format[] variantFormats = new Format[variants.length]; int[] initialTrackSelection = new int[variants.length]; for (int i = 0; i < variants.length; i++) { @@ -136,6 +144,9 @@ import java.util.List; initialTrackSelection[i] = i; } mediaDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_MEDIA); + if (mediaTransferListener != null) { + mediaDataSource.addTransferListener(mediaTransferListener); + } encryptionDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_DRM); trackGroup = new TrackGroup(variantFormats); trackSelection = new InitializationTrackSelection(trackGroup, initialTrackSelection); @@ -204,18 +215,20 @@ import java.util.List; * but the end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to * contain the {@link HlsUrl} that refers to the playlist that needs refreshing. * - * @param previous The most recently loaded media chunk. * @param playbackPositionUs The current playback position relative to the period start in * microseconds. If playback of the period to which this chunk source belongs has not yet * started, the value will be the starting position in the period minus the duration of any * media in previous periods still to be played. * @param loadPositionUs The current load position relative to the period start in microseconds. - * If {@code previous} is null, this is the starting position from which chunks should be - * provided. Else it's equal to {@code previous.endTimeUs}. + * If {@code queue} is empty, this is the starting position from which chunks should be + * provided. Else it's equal to {@link HlsMediaChunk#endTimeUs} of the last chunk in the + * {@code queue}. + * @param queue The queue of buffered {@link HlsMediaChunk}s. * @param out A holder to populate. */ public void getNextChunk( - HlsMediaChunk previous, long playbackPositionUs, long loadPositionUs, HlsChunkHolder out) { + long playbackPositionUs, long loadPositionUs, List queue, HlsChunkHolder out) { + HlsMediaChunk previous = queue.isEmpty() ? null : queue.get(queue.size() - 1); int oldVariantIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); long bufferedDurationUs = loadPositionUs - playbackPositionUs; @@ -248,22 +261,23 @@ import java.util.List; return; } HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); - independentSegments = mediaPlaylist.hasIndependentSegmentsTag; + independentSegments = mediaPlaylist.hasIndependentSegments; updateLiveEdgeTimeUs(mediaPlaylist); // Select the chunk. long chunkMediaSequence; + long startOfPlaylistInPeriodUs = + mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); if (previous == null || switchingVariant) { - long targetPositionUs = (previous == null || independentSegments) ? loadPositionUs - : previous.startTimeUs; - if (!mediaPlaylist.hasEndTag && targetPositionUs >= mediaPlaylist.getEndTimeUs()) { + long endOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs + mediaPlaylist.durationUs; + long targetPositionInPeriodUs = + (previous == null || independentSegments) ? loadPositionUs : previous.startTimeUs; + if (!mediaPlaylist.hasEndTag && targetPositionInPeriodUs >= endOfPlaylistInPeriodUs) { // If the playlist is too old to contain the chunk, we need to refresh it. chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); } else { - long positionOfPlaylistInPeriodUs = - mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); - long targetPositionInPlaylistUs = targetPositionUs - positionOfPlaylistInPeriodUs; + long targetPositionInPlaylistUs = targetPositionInPeriodUs - startOfPlaylistInPeriodUs; chunkMediaSequence = Util.binarySearchFloor( mediaPlaylist.segments, @@ -277,6 +291,8 @@ import java.util.List; selectedVariantIndex = oldVariantIndex; selectedUrl = variants[selectedVariantIndex]; mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); + startOfPlaylistInPeriodUs = + mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); chunkMediaSequence = previous.getNextChunkIndex(); } } @@ -331,9 +347,7 @@ import java.util.List; } // Compute start time of the next chunk. - long positionOfPlaylistInPeriodUs = - mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); - long segmentStartTimeInPeriodUs = positionOfPlaylistInPeriodUs + segment.relativeStartTimeUs; + long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + segment.relativeStartTimeUs; int discontinuitySequence = mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence; TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( @@ -361,7 +375,7 @@ import java.util.List; isTimestampMaster, timestampAdjuster, previous, - mediaPlaylist.drmInitData, + segment.drmInitData, encryptionKey, encryptionIv); } @@ -382,27 +396,28 @@ import java.util.List; } /** - * Called when the {@link HlsSampleStreamWrapper} encounters an error loading a chunk obtained - * from this source. + * Attempts to blacklist the track associated with the given chunk. Blacklisting will fail if the + * track is the only non-blacklisted track in the selection. * - * @param chunk The chunk whose load encountered the error. - * @param cancelable Whether the load can be canceled. - * @param error The error. - * @return Whether the load should be canceled. + * @param chunk The chunk whose load caused the blacklisting attempt. + * @param blacklistDurationMs The number of milliseconds for which the track selection should be + * blacklisted. + * @return Whether the blacklisting succeeded. */ - public boolean onChunkLoadError(Chunk chunk, boolean cancelable, IOException error) { - return cancelable && ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection, - trackSelection.indexOf(trackGroup.indexOf(chunk.trackFormat)), error); + public boolean maybeBlacklistTrack(Chunk chunk, long blacklistDurationMs) { + return trackSelection.blacklist( + trackSelection.indexOf(trackGroup.indexOf(chunk.trackFormat)), blacklistDurationMs); } /** * Called when a playlist load encounters an error. * * @param url The url of the playlist whose load encountered an error. - * @param shouldBlacklist Whether the playlist should be blacklisted. + * @param blacklistDurationMs The duration for which the playlist should be blacklisted. Or {@link + * C#TIME_UNSET} if the playlist should not be blacklisted. * @return True if blacklisting did not encounter errors. False otherwise. */ - public boolean onPlaylistError(HlsUrl url, boolean shouldBlacklist) { + public boolean onPlaylistError(HlsUrl url, long blacklistDurationMs) { int trackGroupIndex = trackGroup.indexOf(url.format); if (trackGroupIndex == C.INDEX_UNSET) { return true; @@ -412,20 +427,24 @@ import java.util.List; return true; } seenExpectedPlaylistError |= expectedPlaylistUrl == url; - return !shouldBlacklist - || trackSelection.blacklist( - trackSelectionIndex, ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS); + return blacklistDurationMs == C.TIME_UNSET + || trackSelection.blacklist(trackSelectionIndex, blacklistDurationMs); } // Private methods. private long resolveTimeToLiveEdgeUs(long playbackPositionUs) { - final boolean resolveTimeToLiveEdgePossible = liveEdgeTimeUs != C.TIME_UNSET; - return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; + final boolean resolveTimeToLiveEdgePossible = liveEdgeInPeriodTimeUs != C.TIME_UNSET; + return resolveTimeToLiveEdgePossible + ? liveEdgeInPeriodTimeUs - playbackPositionUs + : C.TIME_UNSET; } private void updateLiveEdgeTimeUs(HlsMediaPlaylist mediaPlaylist) { - liveEdgeTimeUs = mediaPlaylist.hasEndTag ? C.TIME_UNSET : mediaPlaylist.getEndTimeUs(); + liveEdgeInPeriodTimeUs = + mediaPlaylist.hasEndTag + ? C.TIME_UNSET + : (mediaPlaylist.getEndTimeUs() - playlistTracker.getInitialStartTimeUs()); } private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex, diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java index 3ed6a549db..a75751815f 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.util.TimestampAdjuster; import java.util.List; +import java.util.Map; /** * Factory for HLS media chunk extractors. @@ -42,12 +43,18 @@ public interface HlsExtractorFactory { * information is available in the master playlist. * @param drmInitData {@link DrmInitData} associated with the chunk. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. + * @param responseHeaders The HTTP response headers associated with the media segment or + * initialization section to extract. * @return A pair containing the {@link Extractor} and a boolean that indicates whether it is a * packed audio extractor. The first element may be {@code previousExtractor} if the factory * has determined it can be re-used. */ - Pair createExtractor(Extractor previousExtractor, Uri uri, Format format, - List muxedCaptionFormats, DrmInitData drmInitData, - TimestampAdjuster timestampAdjuster); - + Pair createExtractor( + Extractor previousExtractor, + Uri uri, + Format format, + List muxedCaptionFormats, + DrmInitData drmInitData, + TimestampAdjuster timestampAdjuster, + Map> responseHeaders); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 9e993aa27b..8c151e59c1 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -69,19 +69,22 @@ import java.util.concurrent.atomic.AtomicInteger; private final boolean hasGapTag; private final TimestampAdjuster timestampAdjuster; private final boolean shouldSpliceIn; - private final Extractor extractor; - private final boolean isPackedAudioExtractor; - private final boolean reusingExtractor; - private final Id3Decoder id3Decoder; - private final ParsableByteArray id3Data; + private final HlsExtractorFactory extractorFactory; + private final List muxedCaptionFormats; + private final DrmInitData drmInitData; + private final Extractor previousExtractor; + private Extractor extractor; + private boolean isPackedAudioExtractor; + private Id3Decoder id3Decoder; + private ParsableByteArray id3Data; private HlsSampleStreamWrapper output; private int initSegmentBytesLoaded; - private int bytesLoaded; + private int nextLoadPosition; private boolean id3TimestampPeeked; private boolean initLoadCompleted; private volatile boolean loadCanceled; - private volatile boolean loadCompleted; + private boolean loadCompleted; /** * @param extractorFactory A {@link HlsExtractorFactory} from which the HLS media chunk extractor @@ -142,35 +145,22 @@ import java.util.concurrent.atomic.AtomicInteger; this.hlsUrl = hlsUrl; this.isMasterTimestampSource = isMasterTimestampSource; this.timestampAdjuster = timestampAdjuster; - // Note: this.dataSource and dataSource may be different. - this.isEncrypted = this.dataSource instanceof Aes128DataSource; + this.isEncrypted = fullSegmentEncryptionKey != null; this.hasGapTag = hasGapTag; + this.extractorFactory = extractorFactory; + this.muxedCaptionFormats = muxedCaptionFormats; + this.drmInitData = drmInitData; Extractor previousExtractor = null; if (previousChunk != null) { + id3Decoder = previousChunk.id3Decoder; + id3Data = previousChunk.id3Data; shouldSpliceIn = previousChunk.hlsUrl != hlsUrl; previousExtractor = previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber || shouldSpliceIn ? null : previousChunk.extractor; } else { shouldSpliceIn = false; } - Pair extractorData = extractorFactory.createExtractor(previousExtractor, - dataSpec.uri, trackFormat, muxedCaptionFormats, drmInitData, timestampAdjuster); - extractor = extractorData.first; - isPackedAudioExtractor = extractorData.second; - reusingExtractor = extractor == previousExtractor; - initLoadCompleted = reusingExtractor && initDataSpec != null; - if (isPackedAudioExtractor) { - if (previousChunk != null && previousChunk.id3Data != null) { - id3Decoder = previousChunk.id3Decoder; - id3Data = previousChunk.id3Data; - } else { - id3Decoder = new Id3Decoder(); - id3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH); - } - } else { - id3Decoder = null; - id3Data = null; - } + this.previousExtractor = previousExtractor; initDataSource = dataSource; uid = uidSource.getAndIncrement(); } @@ -183,10 +173,6 @@ import java.util.concurrent.atomic.AtomicInteger; */ public void init(HlsSampleStreamWrapper output) { this.output = output; - output.init(uid, shouldSpliceIn, reusingExtractor); - if (!reusingExtractor) { - extractor.init(output); - } } @Override @@ -194,11 +180,6 @@ import java.util.concurrent.atomic.AtomicInteger; return loadCompleted; } - @Override - public long bytesLoaded() { - return bytesLoaded; - } - // Loadable implementation @Override @@ -206,11 +187,6 @@ import java.util.concurrent.atomic.AtomicInteger; loadCanceled = true; } - @Override - public boolean isLoadCanceled() { - return loadCanceled; - } - @Override public void load() throws IOException, InterruptedException { maybeLoadInitData(); @@ -222,7 +198,7 @@ import java.util.concurrent.atomic.AtomicInteger; } } - // Internal loading methods. + // Internal methods. private void maybeLoadInitData() throws IOException, InterruptedException { if (initLoadCompleted || initDataSpec == null) { @@ -231,8 +207,7 @@ import java.util.concurrent.atomic.AtomicInteger; } DataSpec initSegmentDataSpec = initDataSpec.subrange(initSegmentBytesLoaded); try { - ExtractorInput input = new DefaultExtractorInput(initDataSource, - initSegmentDataSpec.absoluteStreamPosition, initDataSource.open(initSegmentDataSpec)); + DefaultExtractorInput input = prepareExtraction(initDataSource, initSegmentDataSpec); try { int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { @@ -242,7 +217,7 @@ import java.util.concurrent.atomic.AtomicInteger; initSegmentBytesLoaded = (int) (input.getPosition() - initDataSpec.absoluteStreamPosition); } } finally { - Util.closeQuietly(dataSource); + Util.closeQuietly(initDataSource); } initLoadCompleted = true; } @@ -256,9 +231,9 @@ import java.util.concurrent.atomic.AtomicInteger; boolean skipLoadedBytes; if (isEncrypted) { loadDataSpec = dataSpec; - skipLoadedBytes = bytesLoaded != 0; + skipLoadedBytes = nextLoadPosition != 0; } else { - loadDataSpec = dataSpec.subrange(bytesLoaded); + loadDataSpec = dataSpec.subrange(nextLoadPosition); skipLoadedBytes = false; } if (!isMasterTimestampSource) { @@ -268,8 +243,7 @@ import java.util.concurrent.atomic.AtomicInteger; timestampAdjuster.setFirstSampleTimestampUs(startTimeUs); } try { - ExtractorInput input = new DefaultExtractorInput(dataSource, - loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); + ExtractorInput input = prepareExtraction(dataSource, loadDataSpec); if (isPackedAudioExtractor && !id3TimestampPeeked) { long id3Timestamp = peekId3PrivTimestamp(input); id3TimestampPeeked = true; @@ -277,7 +251,7 @@ import java.util.concurrent.atomic.AtomicInteger; ? timestampAdjuster.adjustTsTimestamp(id3Timestamp) : startTimeUs); } if (skipLoadedBytes) { - input.skipFully(bytesLoaded); + input.skipFully(nextLoadPosition); } try { int result = Extractor.RESULT_CONTINUE; @@ -285,13 +259,44 @@ import java.util.concurrent.atomic.AtomicInteger; result = extractor.read(input, null); } } finally { - bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); + nextLoadPosition = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); } } finally { Util.closeQuietly(dataSource); } } + private DefaultExtractorInput prepareExtraction(DataSource dataSource, DataSpec dataSpec) + throws IOException { + long bytesToRead = dataSource.open(dataSpec); + + if (extractor == null) { + Pair extractorData = + extractorFactory.createExtractor( + previousExtractor, + dataSpec.uri, + trackFormat, + muxedCaptionFormats, + drmInitData, + timestampAdjuster, + dataSource.getResponseHeaders()); + extractor = extractorData.first; + isPackedAudioExtractor = extractorData.second; + boolean reusingExtractor = extractor == previousExtractor; + initLoadCompleted = reusingExtractor && initDataSpec != null; + if (isPackedAudioExtractor && id3Data == null) { + id3Decoder = new Id3Decoder(); + id3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH); + } + output.init(uid, shouldSpliceIn, reusingExtractor); + if (!reusingExtractor) { + extractor.init(output); + } + } + + return new DefaultExtractorInput(dataSource, dataSpec.absoluteStreamPosition, bytesToRead); + } + /** * Peek the presentation timestamp of the first sample in the chunk from an ID3 PRIV as defined * in the HLS spec, version 20, Section 3.4. Returns {@link C#TIME_UNSET} if the frame is not diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 1a3f41fffc..da50d7cc93 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -15,9 +15,11 @@ */ package com.google.android.exoplayer2.source.hls; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -30,6 +32,9 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUr import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -49,7 +54,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final HlsExtractorFactory extractorFactory; private final HlsPlaylistTracker playlistTracker; private final HlsDataSourceFactory dataSourceFactory; - private final int minLoadableRetryCount; + private final @Nullable TransferListener mediaTransferListener; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Allocator allocator; private final IdentityHashMap streamWrapperIndices; @@ -57,7 +63,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final boolean allowChunklessPreparation; - private Callback callback; + private @Nullable Callback callback; private int pendingPrepareCount; private TrackGroupArray trackGroups; private HlsSampleStreamWrapper[] sampleStreamWrappers; @@ -65,11 +71,28 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private SequenceableLoader compositeSequenceableLoader; private boolean notifiedReadingStarted; + /** + * Creates an HLS media period. + * + * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the segments. + * @param playlistTracker A tracker for HLS playlists. + * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for segments + * and keys. + * @param mediaTransferListener The transfer listener to inform of any media data transfers. May + * be null if no listener is available. + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. + * @param eventDispatcher A dispatcher to notify of events. + * @param allocator An {@link Allocator} from which to obtain media buffer allocations. + * @param compositeSequenceableLoaderFactory A factory to create composite {@link + * SequenceableLoader}s for when this media source loads data from multiple streams. + * @param allowChunklessPreparation Whether chunkless preparation is allowed. + */ public HlsMediaPeriod( HlsExtractorFactory extractorFactory, HlsPlaylistTracker playlistTracker, HlsDataSourceFactory dataSourceFactory, - int minLoadableRetryCount, + @Nullable TransferListener mediaTransferListener, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, Allocator allocator, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, @@ -77,7 +100,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; this.dataSourceFactory = dataSourceFactory; - this.minLoadableRetryCount = minLoadableRetryCount; + this.mediaTransferListener = mediaTransferListener; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.allocator = allocator; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; @@ -96,6 +120,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { sampleStreamWrapper.release(); } + callback = null; eventDispatcher.mediaPeriodReleased(); } @@ -305,10 +330,10 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } @Override - public boolean onPlaylistError(HlsUrl url, boolean shouldBlacklist) { + public boolean onPlaylistError(HlsUrl url, long blacklistDurationMs) { boolean noBlacklistingFailure = true; for (HlsSampleStreamWrapper streamWrapper : sampleStreamWrappers) { - noBlacklistingFailure &= streamWrapper.onPlaylistError(url, shouldBlacklist); + noBlacklistingFailure &= streamWrapper.onPlaylistError(url, blacklistDurationMs); } callback.onContinueLoadingRequested(this); return noBlacklistingFailure; @@ -338,7 +363,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper C.TRACK_TYPE_AUDIO, new HlsUrl[] {audioRendition}, null, - Collections.emptyList(), + Collections.emptyList(), positionUs); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; Format renditionFormat = audioRendition.format; @@ -355,11 +380,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper HlsUrl url = subtitleRenditions.get(i); HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper( - C.TRACK_TYPE_TEXT, - new HlsUrl[] {url}, - null, - Collections.emptyList(), - positionUs); + C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null, Collections.emptyList(), positionUs); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.prepareWithMasterPlaylistInfo( new TrackGroupArray(new TrackGroup(url.format)), 0, TrackGroupArray.EMPTY); @@ -440,8 +461,10 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper && (masterPlaylist.muxedAudioFormat != null || masterPlaylist.audios.isEmpty())) { muxedTrackGroups.add( new TrackGroup( - deriveMuxedAudioFormat( - variants[0].format, masterPlaylist.muxedAudioFormat, Format.NO_VALUE))); + deriveAudioFormat( + variants[0].format, + masterPlaylist.muxedAudioFormat, + /* isPrimaryTrackInVariant= */ false))); } List ccFormats = masterPlaylist.muxedCaptionFormats; if (ccFormats != null) { @@ -455,8 +478,10 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper for (int i = 0; i < audioFormats.length; i++) { Format variantFormat = variants[i].format; audioFormats[i] = - deriveMuxedAudioFormat( - variantFormat, masterPlaylist.muxedAudioFormat, variantFormat.bitrate); + deriveAudioFormat( + variantFormat, + masterPlaylist.muxedAudioFormat, + /* isPrimaryTrackInVariant= */ true); } muxedTrackGroups.add(new TrackGroup(audioFormats)); } else { @@ -486,53 +511,77 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, Format muxedAudioFormat, List muxedCaptionFormats, long positionUs) { - HlsChunkSource defaultChunkSource = new HlsChunkSource(extractorFactory, playlistTracker, - variants, dataSourceFactory, timestampAdjusterProvider, muxedCaptionFormats); - return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, positionUs, - muxedAudioFormat, minLoadableRetryCount, eventDispatcher); + HlsChunkSource defaultChunkSource = + new HlsChunkSource( + extractorFactory, + playlistTracker, + variants, + dataSourceFactory, + mediaTransferListener, + timestampAdjusterProvider, + muxedCaptionFormats); + return new HlsSampleStreamWrapper( + trackType, + /* callback= */ this, + defaultChunkSource, + allocator, + positionUs, + muxedAudioFormat, + loadErrorHandlingPolicy, + eventDispatcher); } private static Format deriveVideoFormat(Format variantFormat) { String codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO); - String mimeType = MimeTypes.getMediaMimeType(codecs); - return Format.createVideoSampleFormat( + String sampleMimeType = MimeTypes.getMediaMimeType(codecs); + return Format.createVideoContainerFormat( variantFormat.id, - mimeType, + variantFormat.label, + variantFormat.containerMimeType, + sampleMimeType, codecs, variantFormat.bitrate, - Format.NO_VALUE, variantFormat.width, variantFormat.height, variantFormat.frameRate, - null, - null); + /* initializationData= */ null, + variantFormat.selectionFlags); } - private static Format deriveMuxedAudioFormat( - Format variantFormat, Format mediaTagFormat, int bitrate) { + private static Format deriveAudioFormat( + Format variantFormat, Format mediaTagFormat, boolean isPrimaryTrackInVariant) { String codecs; int channelCount = Format.NO_VALUE; int selectionFlags = 0; String language = null; + String label = null; if (mediaTagFormat != null) { codecs = mediaTagFormat.codecs; channelCount = mediaTagFormat.channelCount; selectionFlags = mediaTagFormat.selectionFlags; language = mediaTagFormat.language; + label = mediaTagFormat.label; } else { codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_AUDIO); + if (isPrimaryTrackInVariant) { + channelCount = variantFormat.channelCount; + selectionFlags = variantFormat.selectionFlags; + language = variantFormat.label; + label = variantFormat.label; + } } - String mimeType = MimeTypes.getMediaMimeType(codecs); - return Format.createAudioSampleFormat( + String sampleMimeType = MimeTypes.getMediaMimeType(codecs); + int bitrate = isPrimaryTrackInVariant ? variantFormat.bitrate : Format.NO_VALUE; + return Format.createAudioContainerFormat( variantFormat.id, - mimeType, + label, + variantFormat.containerMimeType, + sampleMimeType, codecs, bitrate, - Format.NO_VALUE, channelCount, - Format.NO_VALUE, - null, - null, + /* sampleRate= */ Format.NO_VALUE, + /* initializationData= */ null, selectionFlags, language); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 01bb36f6ce..1000a38820 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -32,13 +32,18 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispat import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.ads.AdsMediaSource; +import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParserFactory; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.List; @@ -58,8 +63,9 @@ public final class HlsMediaSource extends BaseMediaSource private HlsExtractorFactory extractorFactory; private @Nullable ParsingLoadable.Parser playlistParser; + private @Nullable HlsPlaylistTracker playlistTracker; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private int minLoadableRetryCount; + private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private boolean allowChunklessPreparation; private boolean isCreateCalled; private @Nullable Object tag; @@ -84,7 +90,7 @@ public final class HlsMediaSource extends BaseMediaSource public Factory(HlsDataSourceFactory hlsDataSourceFactory) { this.hlsDataSourceFactory = Assertions.checkNotNull(hlsDataSourceFactory); extractorFactory = HlsExtractorFactory.DEFAULT; - minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; + loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } @@ -118,17 +124,42 @@ public final class HlsMediaSource extends BaseMediaSource return this; } + /** + * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link + * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. + * + *

    Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}. + * + *

    If {@link #setPlaylistTracker} is not called on this builder, {@code + * loadErrorHandlingPolicy} is used for creating the used {@link DefaultHlsPlaylistTracker}. + * + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + Assertions.checkState(!isCreateCalled); + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + return this; + } + /** * Sets the minimum number of times to retry if a loading error occurs. The default value is - * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * {@link DefaultLoadErrorHandlingPolicy#DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * + *

    Calling this method is equivalent to calling {@link #setLoadErrorHandlingPolicy} with + * {@link DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy(int) + * DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)} * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead. */ + @Deprecated public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { Assertions.checkState(!isCreateCalled); - this.minLoadableRetryCount = minLoadableRetryCount; + this.loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount); return this; } @@ -136,16 +167,41 @@ public final class HlsMediaSource extends BaseMediaSource * Sets the parser to parse HLS playlists. The default is an instance of {@link * HlsPlaylistParser}. * + *

    Must not be called after calling {@link #setPlaylistTracker} on the same builder. + * * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @deprecated Use {@link #setPlaylistTracker(HlsPlaylistTracker)} instead. Using this method + * prevents support for attributes that are carried over from the master playlist to the + * media playlists. */ + @Deprecated public Factory setPlaylistParser(ParsingLoadable.Parser playlistParser) { Assertions.checkState(!isCreateCalled); + Assertions.checkState(playlistTracker == null, "A playlist tracker has already been set."); this.playlistParser = Assertions.checkNotNull(playlistParser); return this; } + /** + * Sets the HLS playlist tracker. The default is an instance of {@link + * DefaultHlsPlaylistTracker}. Playlist trackers must not be shared by {@link HlsMediaSource} + * instances. + * + *

    Must not be called after calling {@link #setPlaylistParser} on the same builder. + * + * @param playlistTracker A tracker for HLS playlists. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setPlaylistTracker(HlsPlaylistTracker playlistTracker) { + Assertions.checkState(!isCreateCalled); + Assertions.checkState(playlistParser == null, "A playlist parser has already been set."); + this.playlistTracker = Assertions.checkNotNull(playlistTracker); + return this; + } + /** * Sets the factory to create composite {@link SequenceableLoader}s for when this media source * loads data from multiple streams (video, audio etc...). The default is an instance of {@link @@ -187,16 +243,24 @@ public final class HlsMediaSource extends BaseMediaSource @Override public HlsMediaSource createMediaSource(Uri playlistUri) { isCreateCalled = true; - if (playlistParser == null) { - playlistParser = new HlsPlaylistParser(); + if (playlistTracker == null) { + if (playlistParser == null) { + playlistTracker = + new DefaultHlsPlaylistTracker( + hlsDataSourceFactory, loadErrorHandlingPolicy, HlsPlaylistParserFactory.DEFAULT); + } else { + playlistTracker = + new DefaultHlsPlaylistTracker( + hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParser); + } } return new HlsMediaSource( playlistUri, hlsDataSourceFactory, extractorFactory, compositeSequenceableLoaderFactory, - minLoadableRetryCount, - playlistParser, + loadErrorHandlingPolicy, + playlistTracker, allowChunklessPreparation, tag); } @@ -221,23 +285,19 @@ public final class HlsMediaSource extends BaseMediaSource public int[] getSupportedTypes() { return new int[] {C.TYPE_HLS}; } - } - /** - * The default minimum number of times to retry loading data prior to failing. - */ - public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; + } private final HlsExtractorFactory extractorFactory; private final Uri manifestUri; private final HlsDataSourceFactory dataSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private final int minLoadableRetryCount; - private final ParsingLoadable.Parser playlistParser; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final boolean allowChunklessPreparation; + private final HlsPlaylistTracker playlistTracker; private final @Nullable Object tag; - private HlsPlaylistTracker playlistTracker; + private @Nullable TransferListener mediaTransferListener; /** * @param manifestUri The {@link Uri} of the HLS manifest. @@ -254,7 +314,11 @@ public final class HlsMediaSource extends BaseMediaSource DataSource.Factory dataSourceFactory, Handler eventHandler, MediaSourceEventListener eventListener) { - this(manifestUri, dataSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, + this( + manifestUri, + dataSourceFactory, + DefaultLoadErrorHandlingPolicy.DEFAULT_MIN_LOADABLE_RETRY_COUNT, + eventHandler, eventListener); } @@ -276,8 +340,13 @@ public final class HlsMediaSource extends BaseMediaSource int minLoadableRetryCount, Handler eventHandler, MediaSourceEventListener eventListener) { - this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory), - HlsExtractorFactory.DEFAULT, minLoadableRetryCount, eventHandler, eventListener, + this( + manifestUri, + new DefaultHlsDataSourceFactory(dataSourceFactory), + HlsExtractorFactory.DEFAULT, + minLoadableRetryCount, + eventHandler, + eventListener, new HlsPlaylistParser()); } @@ -308,8 +377,11 @@ public final class HlsMediaSource extends BaseMediaSource dataSourceFactory, extractorFactory, new DefaultCompositeSequenceableLoaderFactory(), - minLoadableRetryCount, - playlistParser, + new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), + new DefaultHlsPlaylistTracker( + dataSourceFactory, + new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), + playlistParser), /* allowChunklessPreparation= */ false, /* tag= */ null); if (eventHandler != null && eventListener != null) { @@ -322,26 +394,28 @@ public final class HlsMediaSource extends BaseMediaSource HlsDataSourceFactory dataSourceFactory, HlsExtractorFactory extractorFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, - int minLoadableRetryCount, - ParsingLoadable.Parser playlistParser, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, + HlsPlaylistTracker playlistTracker, boolean allowChunklessPreparation, @Nullable Object tag) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.extractorFactory = extractorFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; - this.minLoadableRetryCount = minLoadableRetryCount; - this.playlistParser = playlistParser; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + this.playlistTracker = playlistTracker; this.allowChunklessPreparation = allowChunklessPreparation; this.tag = tag; } @Override - public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + public void prepareSourceInternal( + ExoPlayer player, + boolean isTopLevelSource, + @Nullable TransferListener mediaTransferListener) { + this.mediaTransferListener = mediaTransferListener; EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); - playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher, - minLoadableRetryCount, this, playlistParser); - playlistTracker.start(); + playlistTracker.start(manifestUri, eventDispatcher, /* listener= */ this); } @Override @@ -357,7 +431,8 @@ public final class HlsMediaSource extends BaseMediaSource extractorFactory, playlistTracker, dataSourceFactory, - minLoadableRetryCount, + mediaTransferListener, + loadErrorHandlingPolicy, eventDispatcher, allocator, compositeSequenceableLoaderFactory, @@ -371,10 +446,7 @@ public final class HlsMediaSource extends BaseMediaSource @Override public void releaseSourceInternal() { - if (playlistTracker != null) { - playlistTracker.release(); - playlistTracker = null; - } + playlistTracker.stop(); } @Override diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index 5d4d953372..f43d119018 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; /** @@ -36,6 +37,11 @@ import java.io.IOException; sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING; } + public void bindSampleQueue() { + Assertions.checkArgument(sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING); + sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex); + } + public void unbindSampleQueue() { if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) { sampleStreamWrapper.unbindSampleQueue(trackGroupIndex); @@ -48,12 +54,11 @@ import java.io.IOException; @Override public boolean isReady() { return sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL - || (maybeMapToSampleQueue() && sampleStreamWrapper.isReady(sampleQueueIndex)); + || (hasValidSampleQueueIndex() && sampleStreamWrapper.isReady(sampleQueueIndex)); } @Override public void maybeThrowError() throws IOException { - maybeMapToSampleQueue(); if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL) { throw new SampleQueueMappingException( sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType); @@ -63,22 +68,21 @@ import java.io.IOException; @Override public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { - return maybeMapToSampleQueue() + return hasValidSampleQueueIndex() ? sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat) : C.RESULT_NOTHING_READ; } @Override public int skipData(long positionUs) { - return maybeMapToSampleQueue() ? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs) : 0; + return hasValidSampleQueueIndex() + ? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs) + : 0; } // Internal methods. - private boolean maybeMapToSampleQueue() { - if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) { - sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex); - } + private boolean hasValidSampleQueueIndex() { return sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 0de4faa9c0..5c63e19f28 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -16,12 +16,10 @@ package com.google.android.exoplayer2.source.hls; import android.os.Handler; -import android.support.annotation.IntDef; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -39,15 +37,17 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.Loader; +import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; /** * Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides @@ -80,28 +80,21 @@ import java.util.Arrays; public static final int SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL = -2; public static final int SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL = -3; - @Retention(RetentionPolicy.SOURCE) - @IntDef({PRIMARY_TYPE_NONE, PRIMARY_TYPE_TEXT, PRIMARY_TYPE_AUDIO, PRIMARY_TYPE_VIDEO}) - private @interface PrimaryTrackType {} - - private static final int PRIMARY_TYPE_NONE = 0; - private static final int PRIMARY_TYPE_TEXT = 1; - private static final int PRIMARY_TYPE_AUDIO = 2; - private static final int PRIMARY_TYPE_VIDEO = 3; - private final int trackType; private final Callback callback; private final HlsChunkSource chunkSource; private final Allocator allocator; private final Format muxedAudioFormat; - private final int minLoadableRetryCount; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final Loader loader; private final EventDispatcher eventDispatcher; private final HlsChunkSource.HlsChunkHolder nextChunkHolder; private final ArrayList mediaChunks; + private final List readOnlyMediaChunks; private final Runnable maybeFinishPrepareRunnable; private final Runnable onTracksEndedRunnable; private final Handler handler; + private final ArrayList hlsSampleStreams; private SampleQueue[] sampleQueues; private int[] sampleQueueTrackIds; @@ -109,9 +102,12 @@ import java.util.Arrays; private int audioSampleQueueIndex; private boolean videoSampleQueueMappingDone; private int videoSampleQueueIndex; + private int primarySampleQueueType; + private int primarySampleQueueIndex; private boolean sampleQueuesBuilt; private boolean prepared; private int enabledTrackGroupCount; + private Format upstreamTrackFormat; private Format downstreamTrackFormat; private boolean released; @@ -135,6 +131,7 @@ import java.util.Arrays; // Accessed only by the loading thread. private boolean tracksEnded; private long sampleOffsetUs; + private int chunkUid; /** * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants. @@ -143,19 +140,24 @@ import java.util.Arrays; * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param positionUs The position from which to start loading media. * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist. - * @param minLoadableRetryCount The minimum number of times that the source should retry a load - * before propagating an error. + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. * @param eventDispatcher A dispatcher to notify of events. */ - public HlsSampleStreamWrapper(int trackType, Callback callback, HlsChunkSource chunkSource, - Allocator allocator, long positionUs, Format muxedAudioFormat, int minLoadableRetryCount, + public HlsSampleStreamWrapper( + int trackType, + Callback callback, + HlsChunkSource chunkSource, + Allocator allocator, + long positionUs, + Format muxedAudioFormat, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher) { this.trackType = trackType; this.callback = callback; this.chunkSource = chunkSource; this.allocator = allocator; this.muxedAudioFormat = muxedAudioFormat; - this.minLoadableRetryCount = minLoadableRetryCount; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; loader = new Loader("Loader:HlsSampleStreamWrapper"); nextChunkHolder = new HlsChunkSource.HlsChunkHolder(); @@ -166,20 +168,10 @@ import java.util.Arrays; sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueuesEnabledStates = new boolean[0]; mediaChunks = new ArrayList<>(); - maybeFinishPrepareRunnable = - new Runnable() { - @Override - public void run() { - maybeFinishPrepare(); - } - }; - onTracksEndedRunnable = - new Runnable() { - @Override - public void run() { - onTracksEnded(); - } - }; + readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); + hlsSampleStreams = new ArrayList<>(); + maybeFinishPrepareRunnable = this::maybeFinishPrepare; + onTracksEndedRunnable = this::onTracksEnded; handler = new Handler(); lastSeekPositionUs = positionUs; pendingResetPositionUs = positionUs; @@ -219,9 +211,6 @@ import java.util.Arrays; } public int bindSampleQueueToSampleStream(int trackGroupIndex) { - if (trackGroupToSampleQueueIndex == null) { - return SAMPLE_QUEUE_INDEX_PENDING; - } int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; if (sampleQueueIndex == C.INDEX_UNSET) { return optionalTrackGroups.indexOf(trackGroups.get(trackGroupIndex)) == C.INDEX_UNSET @@ -295,6 +284,9 @@ import java.util.Arrays; } streams[i] = new HlsSampleStream(this, trackGroupIndex); streamResetFlags[i] = true; + if (trackGroupToSampleQueueIndex != null) { + ((HlsSampleStream) streams[i]).bindSampleQueue(); + } // If there's still a chance of avoiding a seek, try and seek within the sample queue. if (sampleQueuesBuilt && !seekRequired) { SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[trackGroupIndex]]; @@ -360,6 +352,7 @@ import java.util.Arrays; } } + updateSampleStreams(streams); seenFirstTrackSelection = true; return seekRequired; } @@ -411,6 +404,7 @@ import java.util.Arrays; loader.release(this); handler.removeCallbacksAndMessages(null); released = true; + hlsSampleStreams.clear(); } @Override @@ -422,8 +416,8 @@ import java.util.Arrays; chunkSource.setIsTimestampMaster(isTimestampMaster); } - public boolean onPlaylistError(HlsUrl url, boolean shouldBlacklist) { - return chunkSource.onPlaylistError(url, shouldBlacklist); + public boolean onPlaylistError(HlsUrl url, long blacklistDurationMs) { + return chunkSource.onPlaylistError(url, blacklistDurationMs); } // SampleStream implementation. @@ -463,8 +457,23 @@ import java.util.Arrays; downstreamTrackFormat = trackFormat; } - return sampleQueues[sampleQueueIndex].read(formatHolder, buffer, requireFormat, loadingFinished, - lastSeekPositionUs); + int result = + sampleQueues[sampleQueueIndex].read( + formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs); + if (result == C.RESULT_FORMAT_READ && sampleQueueIndex == primarySampleQueueIndex) { + // Fill in primary sample format with information from the track format. + int chunkUid = sampleQueues[sampleQueueIndex].peekSourceId(); + int chunkIndex = 0; + while (chunkIndex < mediaChunks.size() && mediaChunks.get(chunkIndex).uid != chunkUid) { + chunkIndex++; + } + Format trackFormat = + chunkIndex < mediaChunks.size() + ? mediaChunks.get(chunkIndex).trackFormat + : upstreamTrackFormat; + formatHolder.format = formatHolder.format.copyWithManifestFormatInfo(trackFormat); + } + return result; } public int skipData(int sampleQueueIndex, long positionUs) { @@ -522,16 +531,16 @@ import java.util.Arrays; return false; } - HlsMediaChunk previousChunk; + List chunkQueue; long loadPositionUs; if (isPendingReset()) { - previousChunk = null; + chunkQueue = Collections.emptyList(); loadPositionUs = pendingResetPositionUs; } else { - previousChunk = getLastMediaChunk(); - loadPositionUs = previousChunk.endTimeUs; + chunkQueue = readOnlyMediaChunks; + loadPositionUs = getLastMediaChunk().endTimeUs; } - chunkSource.getNextChunk(previousChunk, positionUs, loadPositionUs, nextChunkHolder); + chunkSource.getNextChunk(positionUs, loadPositionUs, chunkQueue, nextChunkHolder); boolean endOfStream = nextChunkHolder.endOfStream; Chunk loadable = nextChunkHolder.chunk; HlsMasterPlaylist.HlsUrl playlistToLoad = nextChunkHolder.playlist; @@ -555,11 +564,22 @@ import java.util.Arrays; HlsMediaChunk mediaChunk = (HlsMediaChunk) loadable; mediaChunk.init(this); mediaChunks.add(mediaChunk); + upstreamTrackFormat = mediaChunk.trackFormat; } - long elapsedRealtimeMs = loader.startLoading(loadable, this, minLoadableRetryCount); - eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs); + long elapsedRealtimeMs = + loader.startLoading( + loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)); + eventDispatcher.loadStarted( + loadable.dataSpec, + loadable.dataSpec.uri, + loadable.type, + trackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs); return true; } @@ -573,9 +593,19 @@ import java.util.Arrays; @Override public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) { chunkSource.onChunkLoadCompleted(loadable); - eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); + eventDispatcher.loadCompleted( + loadable.dataSpec, + loadable.getUri(), + loadable.type, + trackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); if (!prepared) { continueLoading(lastSeekPositionUs); } else { @@ -586,9 +616,19 @@ import java.util.Arrays; @Override public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); + eventDispatcher.loadCanceled( + loadable.dataSpec, + loadable.getUri(), + loadable.type, + trackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); if (!released) { resetSampleQueues(); if (enabledTrackGroupCount > 0) { @@ -598,13 +638,27 @@ import java.util.Arrays; } @Override - public @Loader.RetryAction int onLoadError( - Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { + public LoadErrorAction onLoadError( + Chunk loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error, + int errorCount) { long bytesLoaded = loadable.bytesLoaded(); boolean isMediaChunk = isMediaChunk(loadable); - boolean cancelable = !isMediaChunk || bytesLoaded == 0; - boolean canceled = false; - if (chunkSource.onChunkLoadError(loadable, cancelable, error)) { + boolean blacklistSucceeded = false; + LoadErrorAction loadErrorAction; + + if (!isMediaChunk || bytesLoaded == 0) { + long blacklistDurationMs = + loadErrorHandlingPolicy.getBlacklistDurationMsFor( + loadable.type, loadDurationMs, error, errorCount); + if (blacklistDurationMs != C.TIME_UNSET) { + blacklistSucceeded = chunkSource.maybeBlacklistTrack(loadable, blacklistDurationMs); + } + } + + if (blacklistSucceeded) { if (isMediaChunk) { HlsMediaChunk removed = mediaChunks.remove(mediaChunks.size() - 1); Assertions.checkState(removed == loadable); @@ -612,22 +666,41 @@ import java.util.Arrays; pendingResetPositionUs = lastSeekPositionUs; } } - canceled = true; + loadErrorAction = Loader.DONT_RETRY; + } else /* did not blacklist */ { + long retryDelayMs = + loadErrorHandlingPolicy.getRetryDelayMsFor( + loadable.type, loadDurationMs, error, errorCount); + loadErrorAction = + retryDelayMs != C.TIME_UNSET + ? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs) + : Loader.DONT_RETRY_FATAL; } - eventDispatcher.loadError(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded(), error, - canceled); - if (canceled) { + + eventDispatcher.loadError( + loadable.dataSpec, + loadable.getUri(), + loadable.type, + trackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded(), + error, + /* wasCanceled= */ !loadErrorAction.isRetry()); + + if (blacklistSucceeded) { if (!prepared) { continueLoading(lastSeekPositionUs); } else { callback.onContinueLoadingRequested(this); } - return Loader.DONT_RETRY; - } else { - return error instanceof ParserException ? Loader.DONT_RETRY_FATAL : Loader.RETRY; } + return loadErrorAction; } // Called by the consuming thread, but only when there is no loading thread. @@ -646,6 +719,7 @@ import java.util.Arrays; audioSampleQueueMappingDone = false; videoSampleQueueMappingDone = false; } + this.chunkUid = chunkUid; for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.sourceId(chunkUid); } @@ -701,6 +775,7 @@ import java.util.Arrays; } SampleQueue trackOutput = new SampleQueue(allocator); trackOutput.setSampleOffsetUs(sampleOffsetUs); + trackOutput.sourceId(chunkUid); trackOutput.setUpstreamFormatChangeListener(this); sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1); sampleQueueTrackIds[trackCount] = id; @@ -717,6 +792,10 @@ import java.util.Arrays; videoSampleQueueMappingDone = true; videoSampleQueueIndex = trackCount; } + if (getTrackTypeScore(type) > getTrackTypeScore(primarySampleQueueType)) { + primarySampleQueueIndex = trackCount; + primarySampleQueueType = type; + } sampleQueuesEnabledStates = Arrays.copyOf(sampleQueuesEnabledStates, trackCount + 1); return trackOutput; } @@ -750,6 +829,15 @@ import java.util.Arrays; // Internal methods. + private void updateSampleStreams(SampleStream[] streams) { + hlsSampleStreams.clear(); + for (SampleStream stream : streams) { + if (stream != null) { + hlsSampleStreams.add((HlsSampleStream) stream); + } + } + } + private boolean finishedReadingChunk(HlsMediaChunk chunk) { int chunkUid = chunk.uid; int sampleQueueCount = sampleQueues.length; @@ -807,6 +895,9 @@ import java.util.Arrays; } } } + for (HlsSampleStream sampleStream : hlsSampleStreams) { + sampleStream.bindSampleQueue(); + } } /** @@ -842,22 +933,22 @@ import java.util.Arrays; private void buildTracksFromSampleStreams() { // Iterate through the extractor tracks to discover the "primary" track type, and the index // of the single track of this type. - @PrimaryTrackType int primaryExtractorTrackType = PRIMARY_TYPE_NONE; + int primaryExtractorTrackType = C.TRACK_TYPE_NONE; int primaryExtractorTrackIndex = C.INDEX_UNSET; int extractorTrackCount = sampleQueues.length; for (int i = 0; i < extractorTrackCount; i++) { String sampleMimeType = sampleQueues[i].getUpstreamFormat().sampleMimeType; - @PrimaryTrackType int trackType; + int trackType; if (MimeTypes.isVideo(sampleMimeType)) { - trackType = PRIMARY_TYPE_VIDEO; + trackType = C.TRACK_TYPE_VIDEO; } else if (MimeTypes.isAudio(sampleMimeType)) { - trackType = PRIMARY_TYPE_AUDIO; + trackType = C.TRACK_TYPE_AUDIO; } else if (MimeTypes.isText(sampleMimeType)) { - trackType = PRIMARY_TYPE_TEXT; + trackType = C.TRACK_TYPE_TEXT; } else { - trackType = PRIMARY_TYPE_NONE; + trackType = C.TRACK_TYPE_NONE; } - if (trackType > primaryExtractorTrackType) { + if (getTrackTypeScore(trackType) > getTrackTypeScore(primaryExtractorTrackType)) { primaryExtractorTrackType = trackType; primaryExtractorTrackIndex = i; } else if (trackType == primaryExtractorTrackType @@ -884,14 +975,21 @@ import java.util.Arrays; Format sampleFormat = sampleQueues[i].getUpstreamFormat(); if (i == primaryExtractorTrackIndex) { Format[] formats = new Format[chunkSourceTrackCount]; - for (int j = 0; j < chunkSourceTrackCount; j++) { - formats[j] = deriveFormat(chunkSourceTrackGroup.getFormat(j), sampleFormat, true); + if (chunkSourceTrackCount == 1) { + formats[0] = sampleFormat.copyWithManifestFormatInfo(chunkSourceTrackGroup.getFormat(0)); + } else { + for (int j = 0; j < chunkSourceTrackCount; j++) { + formats[j] = deriveFormat(chunkSourceTrackGroup.getFormat(j), sampleFormat, true); + } } trackGroups[i] = new TrackGroup(formats); primaryTrackGroupIndex = i; } else { - Format trackFormat = primaryExtractorTrackType == PRIMARY_TYPE_VIDEO - && MimeTypes.isAudio(sampleFormat.sampleMimeType) ? muxedAudioFormat : null; + Format trackFormat = + primaryExtractorTrackType == C.TRACK_TYPE_VIDEO + && MimeTypes.isAudio(sampleFormat.sampleMimeType) + ? muxedAudioFormat + : null; trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat, false)); } } @@ -933,7 +1031,28 @@ import java.util.Arrays; } /** - * Derives a track format using master playlist and sample format information. + * Scores a track type. Where multiple tracks are muxed into a container, the track with the + * highest score is the primary track. + * + * @param trackType The track type. + * @return The score. + */ + private static int getTrackTypeScore(int trackType) { + switch (trackType) { + case C.TRACK_TYPE_VIDEO: + return 3; + case C.TRACK_TYPE_AUDIO: + return 2; + case C.TRACK_TYPE_TEXT: + return 1; + default: + return 0; + } + } + + /** + * Derives a track sample format from the corresponding format in the master playlist, and a + * sample format that may have been obtained from a chunk belonging to a different track. * * @param playlistFormat The format information obtained from the master playlist. * @param sampleFormat The format information obtained from the samples. @@ -955,6 +1074,7 @@ import java.util.Arrays; } return sampleFormat.copyWithContainerInfo( playlistFormat.id, + playlistFormat.label, mimeType, codecs, bitrate, diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java index 2d430d2c79..9c9cb532a6 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.hls; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.TrackGroup; import java.io.IOException; @@ -23,7 +24,7 @@ import java.io.IOException; public final class SampleQueueMappingException extends IOException { /** @param mimeType The mime type of the track group whose mapping failed. */ - public SampleQueueMappingException(String mimeType) { + public SampleQueueMappingException(@Nullable String mimeType) { super("Unable to bind a sample queue to TrackGroup with mime type " + mimeType + "."); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java index 0b8f7f36a6..da6e0f94ad 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java @@ -37,13 +37,13 @@ import java.util.regex.Pattern; /** * A special purpose extractor for WebVTT content in HLS. - *

    - * This extractor passes through non-empty WebVTT files untouched, however derives the correct + * + *

    This extractor passes through non-empty WebVTT files untouched, however derives the correct * sample timestamp for each by sniffing the X-TIMESTAMP-MAP header along with the start timestamp * of the first cue header. Empty WebVTT files are not passed through, since it's not possible to * derive a sample timestamp in this case. */ -/* package */ final class WebvttExtractor implements Extractor { +public final class WebvttExtractor implements Extractor { private static final Pattern LOCAL_TIMESTAMP = Pattern.compile("LOCAL:([^,]+)"); private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:(\\d+)"); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java index e56bf66efd..c54a9a7dd3 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java @@ -20,56 +20,78 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.SegmentDownloadAction; -import com.google.android.exoplayer2.source.hls.playlist.RenditionKey; +import com.google.android.exoplayer2.offline.StreamKey; import java.io.DataInputStream; -import java.io.DataOutputStream; import java.io.IOException; +import java.util.Collections; import java.util.List; /** An action to download or remove downloaded HLS streams. */ -public final class HlsDownloadAction extends SegmentDownloadAction { +public final class HlsDownloadAction extends SegmentDownloadAction { private static final String TYPE = "hls"; - private static final int VERSION = 0; + private static final int VERSION = 1; public static final Deserializer DESERIALIZER = - new SegmentDownloadActionDeserializer(TYPE, VERSION) { + new SegmentDownloadActionDeserializer(TYPE, VERSION) { @Override - protected RenditionKey readKey(DataInputStream input) throws IOException { + protected StreamKey readKey(int version, DataInputStream input) throws IOException { + if (version > 0) { + return super.readKey(version, input); + } int renditionGroup = input.readInt(); int trackIndex = input.readInt(); - return new RenditionKey(renditionGroup, trackIndex); + return new StreamKey(renditionGroup, trackIndex); } @Override protected DownloadAction createDownloadAction( - Uri uri, boolean isRemoveAction, byte[] data, List keys) { + Uri uri, boolean isRemoveAction, byte[] data, List keys) { return new HlsDownloadAction(uri, isRemoveAction, data, keys); } }; + /** + * Creates a HLS download action. + * + * @param uri The URI of the media to be downloaded. + * @param data Optional custom data for this action. If {@code null} an empty array will be used. + * @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded. + */ + public static HlsDownloadAction createDownloadAction( + Uri uri, @Nullable byte[] data, List keys) { + return new HlsDownloadAction(uri, /* isRemoveAction= */ false, data, keys); + } + + /** + * Creates a HLS remove action. + * + * @param uri The URI of the media to be removed. + * @param data Optional custom data for this action. If {@code null} an empty array will be used. + */ + public static HlsDownloadAction createRemoveAction(Uri uri, @Nullable byte[] data) { + return new HlsDownloadAction(uri, /* isRemoveAction= */ true, data, Collections.emptyList()); + } + /** * @param uri The HLS playlist URI. * @param isRemoveAction Whether the data will be removed. If {@code false} it will be downloaded. * @param data Optional custom data for this action. * @param keys Keys of renditions to be downloaded. If empty, all renditions are downloaded. If * {@code removeAction} is true, {@code keys} must empty. + * @deprecated Use {@link #createDownloadAction(Uri, byte[], List)} or {@link + * #createRemoveAction(Uri, byte[])}. */ + @Deprecated public HlsDownloadAction( - Uri uri, boolean isRemoveAction, @Nullable byte[] data, List keys) { + Uri uri, boolean isRemoveAction, @Nullable byte[] data, List keys) { super(TYPE, VERSION, uri, isRemoveAction, data, keys); } @Override - protected HlsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { + public HlsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { return new HlsDownloader(uri, keys, constructorHelper); } - @Override - protected void writeKey(DataOutputStream output, RenditionKey key) throws IOException { - output.writeInt(key.type); - output.writeInt(key.trackIndex); - } - } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java index 37aa181970..fcbe06993e 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java @@ -17,8 +17,10 @@ package com.google.android.exoplayer2.source.hls.offline; import android.net.Uri; import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.TrackKey; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -26,14 +28,12 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; -import com.google.android.exoplayer2.source.hls.playlist.RenditionKey; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -44,7 +44,7 @@ public final class HlsDownloadHelper extends DownloadHelper { private final DataSource.Factory manifestDataSourceFactory; private @MonotonicNonNull HlsPlaylist playlist; - private int[] renditionTypes; + private int[] renditionGroups; public HlsDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) { this.uri = uri; @@ -54,7 +54,7 @@ public final class HlsDownloadHelper extends DownloadHelper { @Override protected void prepareInternal() throws IOException { DataSource dataSource = manifestDataSourceFactory.createDataSource(); - playlist = ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri); + playlist = ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri, C.DATA_TYPE_MANIFEST); } /** Returns the HLS playlist. Must not be called until after preparation completes. */ @@ -73,23 +73,24 @@ public final class HlsDownloadHelper extends DownloadHelper { public TrackGroupArray getTrackGroups(int periodIndex) { Assertions.checkNotNull(playlist); if (playlist instanceof HlsMediaPlaylist) { + renditionGroups = new int[0]; return TrackGroupArray.EMPTY; } // TODO: Generate track groups as in playback. Reverse the mapping in getDownloadAction. HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; TrackGroup[] trackGroups = new TrackGroup[3]; - renditionTypes = new int[3]; + renditionGroups = new int[3]; int trackGroupIndex = 0; if (!masterPlaylist.variants.isEmpty()) { - renditionTypes[trackGroupIndex] = RenditionKey.TYPE_VARIANT; + renditionGroups[trackGroupIndex] = HlsMasterPlaylist.GROUP_INDEX_VARIANT; trackGroups[trackGroupIndex++] = new TrackGroup(toFormats(masterPlaylist.variants)); } if (!masterPlaylist.audios.isEmpty()) { - renditionTypes[trackGroupIndex] = RenditionKey.TYPE_AUDIO; + renditionGroups[trackGroupIndex] = HlsMasterPlaylist.GROUP_INDEX_AUDIO; trackGroups[trackGroupIndex++] = new TrackGroup(toFormats(masterPlaylist.audios)); } if (!masterPlaylist.subtitles.isEmpty()) { - renditionTypes[trackGroupIndex] = RenditionKey.TYPE_SUBTITLE; + renditionGroups[trackGroupIndex] = HlsMasterPlaylist.GROUP_INDEX_SUBTITLE; trackGroups[trackGroupIndex++] = new TrackGroup(toFormats(masterPlaylist.subtitles)); } return new TrackGroupArray(Arrays.copyOf(trackGroups, trackGroupIndex)); @@ -97,15 +98,14 @@ public final class HlsDownloadHelper extends DownloadHelper { @Override public HlsDownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { - Assertions.checkNotNull(renditionTypes); - return new HlsDownloadAction( - uri, /* isRemoveAction= */ false, data, toRenditionKeys(trackKeys, renditionTypes)); + Assertions.checkNotNull(renditionGroups); + return HlsDownloadAction.createDownloadAction( + uri, data, toStreamKeys(trackKeys, renditionGroups)); } @Override public HlsDownloadAction getRemoveAction(@Nullable byte[] data) { - return new HlsDownloadAction( - uri, /* isRemoveAction= */ true, data, Collections.emptyList()); + return HlsDownloadAction.createRemoveAction(uri, data); } private static Format[] toFormats(List hlsUrls) { @@ -116,11 +116,11 @@ public final class HlsDownloadHelper extends DownloadHelper { return formats; } - private static List toRenditionKeys(List trackKeys, int[] groups) { - List representationKeys = new ArrayList<>(trackKeys.size()); + private static List toStreamKeys(List trackKeys, int[] groups) { + List representationKeys = new ArrayList<>(trackKeys.size()); for (int i = 0; i < trackKeys.size(); i++) { TrackKey trackKey = trackKeys.get(i); - representationKeys.add(new RenditionKey(groups[trackKey.groupIndex], trackKey.trackIndex)); + representationKeys.add(new StreamKey(groups[trackKey.groupIndex], trackKey.trackIndex)); } return representationKeys; } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java index bd59eed447..85f41df359 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java @@ -19,12 +19,12 @@ import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.SegmentDownloader; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; -import com.google.android.exoplayer2.source.hls.playlist.RenditionKey; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.ParsingLoadable; @@ -48,7 +48,7 @@ import java.util.List; * HlsDownloader hlsDownloader = * new HlsDownloader( * playlistUri, - * Collections.singletonList(new RenditionKey(RenditionKey.TYPE_VARIANT, 0)), + * Collections.singletonList(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, 0)), * constructorHelper); * // Perform the download. * hlsDownloader.download(); @@ -57,19 +57,17 @@ import java.util.List; * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE); * } */ -public final class HlsDownloader extends SegmentDownloader { +public final class HlsDownloader extends SegmentDownloader { /** * @param playlistUri The {@link Uri} of the playlist to be downloaded. - * @param renditionKeys Keys defining which renditions in the playlist should be selected for + * @param streamKeys Keys defining which renditions in the playlist should be selected for * download. If empty, all renditions are downloaded. * @param constructorHelper A {@link DownloaderConstructorHelper} instance. */ public HlsDownloader( - Uri playlistUri, - List renditionKeys, - DownloaderConstructorHelper constructorHelper) { - super(playlistUri, renditionKeys, constructorHelper); + Uri playlistUri, List streamKeys, DownloaderConstructorHelper constructorHelper) { + super(playlistUri, streamKeys, constructorHelper); } @Override @@ -120,10 +118,7 @@ public final class HlsDownloader extends SegmentDownloader loadable = - new ParsingLoadable<>(dataSource, uri, C.DATA_TYPE_MANIFEST, new HlsPlaylistParser()); - loadable.load(); - return loadable.getResult(); + return ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri, C.DATA_TYPE_MANIFEST); } private static void addSegment( diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java new file mode 100644 index 0000000000..a61c8116ac --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java @@ -0,0 +1,672 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.hls.playlist; + +import android.net.Uri; +import android.os.Handler; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; +import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.Loader; +import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.UriUtil; +import java.io.IOException; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; + +/** Default implementation for {@link HlsPlaylistTracker}. */ +public final class DefaultHlsPlaylistTracker + implements HlsPlaylistTracker, Loader.Callback> { + + /** + * Coefficient applied on the target duration of a playlist to determine the amount of time after + * which an unchanging playlist is considered stuck. + */ + private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 3.5; + + private final HlsDataSourceFactory dataSourceFactory; + private final HlsPlaylistParserFactory playlistParserFactory; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; + private final IdentityHashMap playlistBundles; + private final List listeners; + + private @Nullable ParsingLoadable.Parser mediaPlaylistParser; + private @Nullable EventDispatcher eventDispatcher; + private @Nullable Loader initialPlaylistLoader; + private @Nullable Handler playlistRefreshHandler; + private @Nullable PrimaryPlaylistListener primaryPlaylistListener; + private @Nullable HlsMasterPlaylist masterPlaylist; + private @Nullable HlsUrl primaryHlsUrl; + private @Nullable HlsMediaPlaylist primaryUrlSnapshot; + private boolean isLive; + private long initialStartTimeUs; + + /** + * @param dataSourceFactory A factory for {@link DataSource} instances. + * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. + * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. + * @deprecated Use {@link #DefaultHlsPlaylistTracker(HlsDataSourceFactory, + * LoadErrorHandlingPolicy, HlsPlaylistParserFactory)} instead. Using this constructor + * prevents support for attributes that are carried over from the master playlist to the media + * playlists. + */ + @Deprecated + public DefaultHlsPlaylistTracker( + HlsDataSourceFactory dataSourceFactory, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, + ParsingLoadable.Parser playlistParser) { + this(dataSourceFactory, loadErrorHandlingPolicy, createFixedFactory(playlistParser)); + } + + /** + * @param dataSourceFactory A factory for {@link DataSource} instances. + * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. + * @param playlistParserFactory An {@link HlsPlaylistParserFactory}. + */ + public DefaultHlsPlaylistTracker( + HlsDataSourceFactory dataSourceFactory, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, + HlsPlaylistParserFactory playlistParserFactory) { + this.dataSourceFactory = dataSourceFactory; + this.playlistParserFactory = playlistParserFactory; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + listeners = new ArrayList<>(); + playlistBundles = new IdentityHashMap<>(); + initialStartTimeUs = C.TIME_UNSET; + } + + // HlsPlaylistTracker implementation. + + @Override + public void start( + Uri initialPlaylistUri, + EventDispatcher eventDispatcher, + PrimaryPlaylistListener primaryPlaylistListener) { + this.playlistRefreshHandler = new Handler(); + this.eventDispatcher = eventDispatcher; + this.primaryPlaylistListener = primaryPlaylistListener; + ParsingLoadable masterPlaylistLoadable = + new ParsingLoadable<>( + dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), + initialPlaylistUri, + C.DATA_TYPE_MANIFEST, + playlistParserFactory.createPlaylistParser()); + Assertions.checkState(initialPlaylistLoader == null); + initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MasterPlaylist"); + long elapsedRealtime = + initialPlaylistLoader.startLoading( + masterPlaylistLoadable, + this, + loadErrorHandlingPolicy.getMinimumLoadableRetryCount(masterPlaylistLoadable.type)); + eventDispatcher.loadStarted( + masterPlaylistLoadable.dataSpec, + masterPlaylistLoadable.dataSpec.uri, + masterPlaylistLoadable.type, + elapsedRealtime); + } + + @Override + public void stop() { + primaryHlsUrl = null; + primaryUrlSnapshot = null; + masterPlaylist = null; + initialStartTimeUs = C.TIME_UNSET; + initialPlaylistLoader.release(); + initialPlaylistLoader = null; + for (MediaPlaylistBundle bundle : playlistBundles.values()) { + bundle.release(); + } + playlistRefreshHandler.removeCallbacksAndMessages(null); + playlistRefreshHandler = null; + playlistBundles.clear(); + } + + @Override + public void addListener(PlaylistEventListener listener) { + listeners.add(listener); + } + + @Override + public void removeListener(PlaylistEventListener listener) { + listeners.remove(listener); + } + + @Override + public HlsMasterPlaylist getMasterPlaylist() { + return masterPlaylist; + } + + @Override + public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) { + HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot(); + if (snapshot != null) { + maybeSetPrimaryUrl(url); + } + return snapshot; + } + + @Override + public long getInitialStartTimeUs() { + return initialStartTimeUs; + } + + @Override + public boolean isSnapshotValid(HlsUrl url) { + return playlistBundles.get(url).isSnapshotValid(); + } + + @Override + public void maybeThrowPrimaryPlaylistRefreshError() throws IOException { + if (initialPlaylistLoader != null) { + initialPlaylistLoader.maybeThrowError(); + } + if (primaryHlsUrl != null) { + maybeThrowPlaylistRefreshError(primaryHlsUrl); + } + } + + @Override + public void maybeThrowPlaylistRefreshError(HlsUrl url) throws IOException { + playlistBundles.get(url).maybeThrowPlaylistRefreshError(); + } + + @Override + public void refreshPlaylist(HlsUrl url) { + playlistBundles.get(url).loadPlaylist(); + } + + @Override + public boolean isLive() { + return isLive; + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted( + ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { + HlsPlaylist result = loadable.getResult(); + HlsMasterPlaylist masterPlaylist; + boolean isMediaPlaylist = result instanceof HlsMediaPlaylist; + if (isMediaPlaylist) { + masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri); + } else /* result instanceof HlsMasterPlaylist */ { + masterPlaylist = (HlsMasterPlaylist) result; + } + this.masterPlaylist = masterPlaylist; + mediaPlaylistParser = playlistParserFactory.createPlaylistParser(masterPlaylist); + primaryHlsUrl = masterPlaylist.variants.get(0); + ArrayList urls = new ArrayList<>(); + urls.addAll(masterPlaylist.variants); + urls.addAll(masterPlaylist.audios); + urls.addAll(masterPlaylist.subtitles); + createBundles(urls); + MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryHlsUrl); + if (isMediaPlaylist) { + // We don't need to load the playlist again. We can use the same result. + primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result, loadDurationMs); + } else { + primaryBundle.loadPlaylist(); + } + eventDispatcher.loadCompleted( + loadable.dataSpec, + loadable.getUri(), + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + } + + @Override + public void onLoadCanceled( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + boolean released) { + eventDispatcher.loadCanceled( + loadable.dataSpec, + loadable.getUri(), + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + } + + @Override + public LoadErrorAction onLoadError( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error, + int errorCount) { + long retryDelayMs = + loadErrorHandlingPolicy.getRetryDelayMsFor( + loadable.type, loadDurationMs, error, errorCount); + boolean isFatal = retryDelayMs == C.TIME_UNSET; + eventDispatcher.loadError( + loadable.dataSpec, + loadable.getUri(), + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded(), + error, + isFatal); + return isFatal + ? Loader.DONT_RETRY_FATAL + : Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs); + } + + // Internal methods. + + private boolean maybeSelectNewPrimaryUrl() { + List variants = masterPlaylist.variants; + int variantsSize = variants.size(); + long currentTimeMs = SystemClock.elapsedRealtime(); + for (int i = 0; i < variantsSize; i++) { + MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i)); + if (currentTimeMs > bundle.blacklistUntilMs) { + primaryHlsUrl = bundle.playlistUrl; + bundle.loadPlaylist(); + return true; + } + } + return false; + } + + private void maybeSetPrimaryUrl(HlsUrl url) { + if (url == primaryHlsUrl + || !masterPlaylist.variants.contains(url) + || (primaryUrlSnapshot != null && primaryUrlSnapshot.hasEndTag)) { + // Ignore if the primary url is unchanged, if the url is not a variant url, or if the last + // primary snapshot contains an end tag. + return; + } + primaryHlsUrl = url; + playlistBundles.get(primaryHlsUrl).loadPlaylist(); + } + + private void createBundles(List urls) { + int listSize = urls.size(); + for (int i = 0; i < listSize; i++) { + HlsUrl url = urls.get(i); + MediaPlaylistBundle bundle = new MediaPlaylistBundle(url); + playlistBundles.put(url, bundle); + } + } + + /** + * Called by the bundles when a snapshot changes. + * + * @param url The url of the playlist. + * @param newSnapshot The new snapshot. + */ + private void onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot) { + if (url == primaryHlsUrl) { + if (primaryUrlSnapshot == null) { + // This is the first primary url snapshot. + isLive = !newSnapshot.hasEndTag; + initialStartTimeUs = newSnapshot.startTimeUs; + } + primaryUrlSnapshot = newSnapshot; + primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot); + } + int listenersSize = listeners.size(); + for (int i = 0; i < listenersSize; i++) { + listeners.get(i).onPlaylistChanged(); + } + } + + private boolean notifyPlaylistError(HlsUrl playlistUrl, long blacklistDurationMs) { + int listenersSize = listeners.size(); + boolean anyBlacklistingFailed = false; + for (int i = 0; i < listenersSize; i++) { + anyBlacklistingFailed |= !listeners.get(i).onPlaylistError(playlistUrl, blacklistDurationMs); + } + return anyBlacklistingFailed; + } + + private HlsMediaPlaylist getLatestPlaylistSnapshot( + HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + if (!loadedPlaylist.isNewerThan(oldPlaylist)) { + if (loadedPlaylist.hasEndTag) { + // If the loaded playlist has an end tag but is not newer than the old playlist then we have + // an inconsistent state. This is typically caused by the server incorrectly resetting the + // media sequence when appending the end tag. We resolve this case as best we can by + // returning the old playlist with the end tag appended. + return oldPlaylist.copyWithEndTag(); + } else { + return oldPlaylist; + } + } + long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist); + int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist); + return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence); + } + + private long getLoadedPlaylistStartTimeUs( + HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + if (loadedPlaylist.hasProgramDateTime) { + return loadedPlaylist.startTimeUs; + } + long primarySnapshotStartTimeUs = + primaryUrlSnapshot != null ? primaryUrlSnapshot.startTimeUs : 0; + if (oldPlaylist == null) { + return primarySnapshotStartTimeUs; + } + int oldPlaylistSize = oldPlaylist.segments.size(); + Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); + if (firstOldOverlappingSegment != null) { + return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs; + } else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) { + return oldPlaylist.getEndTimeUs(); + } else { + // No segments overlap, we assume the new playlist start coincides with the primary playlist. + return primarySnapshotStartTimeUs; + } + } + + private int getLoadedPlaylistDiscontinuitySequence( + HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + if (loadedPlaylist.hasDiscontinuitySequence) { + return loadedPlaylist.discontinuitySequence; + } + // TODO: Improve cross-playlist discontinuity adjustment. + int primaryUrlDiscontinuitySequence = + primaryUrlSnapshot != null ? primaryUrlSnapshot.discontinuitySequence : 0; + if (oldPlaylist == null) { + return primaryUrlDiscontinuitySequence; + } + Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); + if (firstOldOverlappingSegment != null) { + return oldPlaylist.discontinuitySequence + + firstOldOverlappingSegment.relativeDiscontinuitySequence + - loadedPlaylist.segments.get(0).relativeDiscontinuitySequence; + } + return primaryUrlDiscontinuitySequence; + } + + private static Segment getFirstOldOverlappingSegment( + HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + int mediaSequenceOffset = (int) (loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence); + List oldSegments = oldPlaylist.segments; + return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null; + } + + /** Holds all information related to a specific Media Playlist. */ + private final class MediaPlaylistBundle + implements Loader.Callback>, Runnable { + + private final HlsUrl playlistUrl; + private final Loader mediaPlaylistLoader; + private final ParsingLoadable mediaPlaylistLoadable; + + private HlsMediaPlaylist playlistSnapshot; + private long lastSnapshotLoadMs; + private long lastSnapshotChangeMs; + private long earliestNextLoadTimeMs; + private long blacklistUntilMs; + private boolean loadPending; + private IOException playlistError; + + public MediaPlaylistBundle(HlsUrl playlistUrl) { + this.playlistUrl = playlistUrl; + mediaPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MediaPlaylist"); + mediaPlaylistLoadable = + new ParsingLoadable<>( + dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), + UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), + C.DATA_TYPE_MANIFEST, + mediaPlaylistParser); + } + + public HlsMediaPlaylist getPlaylistSnapshot() { + return playlistSnapshot; + } + + public boolean isSnapshotValid() { + if (playlistSnapshot == null) { + return false; + } + long currentTimeMs = SystemClock.elapsedRealtime(); + long snapshotValidityDurationMs = Math.max(30000, C.usToMs(playlistSnapshot.durationUs)); + return playlistSnapshot.hasEndTag + || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT + || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD + || lastSnapshotLoadMs + snapshotValidityDurationMs > currentTimeMs; + } + + public void release() { + mediaPlaylistLoader.release(); + } + + public void loadPlaylist() { + blacklistUntilMs = 0; + if (loadPending || mediaPlaylistLoader.isLoading()) { + // Load already pending or in progress. Do nothing. + return; + } + long currentTimeMs = SystemClock.elapsedRealtime(); + if (currentTimeMs < earliestNextLoadTimeMs) { + loadPending = true; + playlistRefreshHandler.postDelayed(this, earliestNextLoadTimeMs - currentTimeMs); + } else { + loadPlaylistImmediately(); + } + } + + public void maybeThrowPlaylistRefreshError() throws IOException { + mediaPlaylistLoader.maybeThrowError(); + if (playlistError != null) { + throw playlistError; + } + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted( + ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { + HlsPlaylist result = loadable.getResult(); + if (result instanceof HlsMediaPlaylist) { + processLoadedPlaylist((HlsMediaPlaylist) result, loadDurationMs); + eventDispatcher.loadCompleted( + loadable.dataSpec, + loadable.getUri(), + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + } else { + playlistError = new ParserException("Loaded playlist has unexpected type."); + } + } + + @Override + public void onLoadCanceled( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + boolean released) { + eventDispatcher.loadCanceled( + loadable.dataSpec, + loadable.getUri(), + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + } + + @Override + public LoadErrorAction onLoadError( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error, + int errorCount) { + LoadErrorAction loadErrorAction; + + long blacklistDurationMs = + loadErrorHandlingPolicy.getBlacklistDurationMsFor( + loadable.type, loadDurationMs, error, errorCount); + boolean shouldBlacklist = blacklistDurationMs != C.TIME_UNSET; + + boolean blacklistingFailed = + notifyPlaylistError(playlistUrl, blacklistDurationMs) || !shouldBlacklist; + if (shouldBlacklist) { + blacklistingFailed |= blacklistPlaylist(blacklistDurationMs); + } + + if (blacklistingFailed) { + long retryDelay = + loadErrorHandlingPolicy.getRetryDelayMsFor( + loadable.type, loadDurationMs, error, errorCount); + loadErrorAction = + retryDelay != C.TIME_UNSET + ? Loader.createRetryAction(false, retryDelay) + : Loader.DONT_RETRY_FATAL; + } else { + loadErrorAction = Loader.DONT_RETRY; + } + + eventDispatcher.loadError( + loadable.dataSpec, + loadable.getUri(), + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded(), + error, + /* wasCanceled= */ !loadErrorAction.isRetry()); + + return loadErrorAction; + } + + // Runnable implementation. + + @Override + public void run() { + loadPending = false; + loadPlaylistImmediately(); + } + + // Internal methods. + + private void loadPlaylistImmediately() { + long elapsedRealtime = + mediaPlaylistLoader.startLoading( + mediaPlaylistLoadable, + this, + loadErrorHandlingPolicy.getMinimumLoadableRetryCount(mediaPlaylistLoadable.type)); + eventDispatcher.loadStarted( + mediaPlaylistLoadable.dataSpec, + mediaPlaylistLoadable.dataSpec.uri, + mediaPlaylistLoadable.type, + elapsedRealtime); + } + + private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist, long loadDurationMs) { + HlsMediaPlaylist oldPlaylist = playlistSnapshot; + long currentTimeMs = SystemClock.elapsedRealtime(); + lastSnapshotLoadMs = currentTimeMs; + playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist); + if (playlistSnapshot != oldPlaylist) { + playlistError = null; + lastSnapshotChangeMs = currentTimeMs; + onPlaylistUpdated(playlistUrl, playlistSnapshot); + } else if (!playlistSnapshot.hasEndTag) { + if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size() + < playlistSnapshot.mediaSequence) { + // TODO: Allow customization of playlist resets handling. + // The media sequence jumped backwards. The server has probably reset. We do not try + // blacklisting in this case. + playlistError = new PlaylistResetException(playlistUrl.url); + notifyPlaylistError(playlistUrl, C.TIME_UNSET); + } else if (currentTimeMs - lastSnapshotChangeMs + > C.usToMs(playlistSnapshot.targetDurationUs) + * PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) { + // TODO: Allow customization of stuck playlists handling. + playlistError = new PlaylistStuckException(playlistUrl.url); + long blacklistDurationMs = + loadErrorHandlingPolicy.getBlacklistDurationMsFor( + C.DATA_TYPE_MANIFEST, loadDurationMs, playlistError, /* errorCount= */ 1); + notifyPlaylistError(playlistUrl, blacklistDurationMs); + if (blacklistDurationMs != C.TIME_UNSET) { + blacklistPlaylist(blacklistDurationMs); + } + } + } + // Do not allow the playlist to load again within the target duration if we obtained a new + // snapshot, or half the target duration otherwise. + earliestNextLoadTimeMs = + currentTimeMs + + C.usToMs( + playlistSnapshot != oldPlaylist + ? playlistSnapshot.targetDurationUs + : (playlistSnapshot.targetDurationUs / 2)); + // Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the + // next load will be scheduled when refreshPlaylist is called, or when this playlist becomes + // the primary. + if (playlistUrl == primaryHlsUrl && !playlistSnapshot.hasEndTag) { + loadPlaylist(); + } + } + + /** + * Blacklists the playlist. + * + * @param blacklistDurationMs The number of milliseconds for which the playlist should be + * blacklisted. + * @return Whether the playlist is the primary, despite being blacklisted. + */ + private boolean blacklistPlaylist(long blacklistDurationMs) { + blacklistUntilMs = SystemClock.elapsedRealtime() + blacklistDurationMs; + return primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl(); + } + } + + /** + * Creates a factory which always returns the given playlist parser. + * + * @param playlistParser The parser to return. + * @return A factory which always returns the given playlist parser. + */ + private static HlsPlaylistParserFactory createFixedFactory( + ParsingLoadable.Parser playlistParser) { + return new HlsPlaylistParserFactory() { + @Override + public ParsingLoadable.Parser createPlaylistParser() { + return playlistParser; + } + + @Override + public ParsingLoadable.Parser createPlaylistParser( + HlsMasterPlaylist masterPlaylist) { + return playlistParser; + } + }; + } +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index 5c29dca38e..c45c2dd547 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.hls.playlist; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Collections; @@ -24,6 +25,22 @@ import java.util.List; /** Represents an HLS master playlist. */ public final class HlsMasterPlaylist extends HlsPlaylist { + /** Represents an empty master playlist, from which no attributes can be inherited. */ + public static final HlsMasterPlaylist EMPTY = + new HlsMasterPlaylist( + /* baseUri= */ "", + /* tags= */ Collections.emptyList(), + /* variants= */ Collections.emptyList(), + /* audios= */ Collections.emptyList(), + /* subtitles= */ Collections.emptyList(), + /* muxedAudioFormat= */ null, + /* muxedCaptionFormats= */ Collections.emptyList(), + /* hasIndependentSegments= */ false); + + public static final int GROUP_INDEX_VARIANT = 0; + public static final int GROUP_INDEX_AUDIO = 1; + public static final int GROUP_INDEX_SUBTITLE = 2; + /** * Represents a url in an HLS master playlist. */ @@ -45,8 +62,16 @@ public final class HlsMasterPlaylist extends HlsPlaylist { * @return An HLS url. */ public static HlsUrl createMediaPlaylistHlsUrl(String url) { - Format format = Format.createContainerFormat("0", MimeTypes.APPLICATION_M3U8, null, null, - Format.NO_VALUE, 0, null); + Format format = + Format.createContainerFormat( + "0", + /* label= */ null, + MimeTypes.APPLICATION_M3U8, + /* sampleMimeType= */ null, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* selectionFlags= */ 0, + /* language= */ null); return new HlsUrl(url, format); } @@ -94,11 +119,18 @@ public final class HlsMasterPlaylist extends HlsPlaylist { * @param subtitles See {@link #subtitles}. * @param muxedAudioFormat See {@link #muxedAudioFormat}. * @param muxedCaptionFormats See {@link #muxedCaptionFormats}. + * @param hasIndependentSegments See {@link #hasIndependentSegments}. */ - public HlsMasterPlaylist(String baseUri, List tags, List variants, - List audios, List subtitles, Format muxedAudioFormat, - List muxedCaptionFormats) { - super(baseUri, tags); + public HlsMasterPlaylist( + String baseUri, + List tags, + List variants, + List audios, + List subtitles, + Format muxedAudioFormat, + List muxedCaptionFormats, + boolean hasIndependentSegments) { + super(baseUri, tags, hasIndependentSegments); this.variants = Collections.unmodifiableList(variants); this.audios = Collections.unmodifiableList(audios); this.subtitles = Collections.unmodifiableList(subtitles); @@ -108,15 +140,16 @@ public final class HlsMasterPlaylist extends HlsPlaylist { } @Override - public HlsMasterPlaylist copy(List renditionKeys) { + public HlsMasterPlaylist copy(List streamKeys) { return new HlsMasterPlaylist( baseUri, tags, - copyRenditionsList(variants, RenditionKey.TYPE_VARIANT, renditionKeys), - copyRenditionsList(audios, RenditionKey.TYPE_AUDIO, renditionKeys), - copyRenditionsList(subtitles, RenditionKey.TYPE_SUBTITLE, renditionKeys), + copyRenditionsList(variants, GROUP_INDEX_VARIANT, streamKeys), + copyRenditionsList(audios, GROUP_INDEX_AUDIO, streamKeys), + copyRenditionsList(subtitles, GROUP_INDEX_SUBTITLE, streamKeys), muxedAudioFormat, - muxedCaptionFormats); + muxedCaptionFormats, + hasIndependentSegments); } /** @@ -128,18 +161,25 @@ public final class HlsMasterPlaylist extends HlsPlaylist { public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUrl) { List variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUrl)); List emptyList = Collections.emptyList(); - return new HlsMasterPlaylist(null, Collections.emptyList(), variant, emptyList, - emptyList, null, null); + return new HlsMasterPlaylist( + null, + Collections.emptyList(), + variant, + emptyList, + emptyList, + /* muxedAudioFormat= */ null, + /* muxedCaptionFormats= */ null, + /* hasIndependentSegments= */ false); } private static List copyRenditionsList( - List renditions, int renditionType, List renditionKeys) { - List copiedRenditions = new ArrayList<>(renditionKeys.size()); + List renditions, int groupIndex, List streamKeys) { + List copiedRenditions = new ArrayList<>(streamKeys.size()); for (int i = 0; i < renditions.size(); i++) { HlsUrl rendition = renditions.get(i); - for (int j = 0; j < renditionKeys.size(); j++) { - RenditionKey renditionKey = renditionKeys.get(j); - if (renditionKey.type == renditionType && renditionKey.trackIndex == i) { + for (int j = 0; j < streamKeys.size(); j++) { + StreamKey streamKey = streamKeys.get(j); + if (streamKey.groupIndex == groupIndex && streamKey.trackIndex == i) { copiedRenditions.add(rendition); break; } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 5ac6f37550..841c13f953 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -20,6 +20,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.offline.StreamKey; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; @@ -41,9 +42,11 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * the media playlist does not define a media section for this segment. The same instance is * used for all segments that share an EXT-X-MAP tag. */ - @Nullable public final Segment initializationSegment; + public final @Nullable Segment initializationSegment; /** The duration of the segment in microseconds, as defined by #EXTINF. */ public final long durationUs; + /** The human readable title of the segment. */ + public final String title; /** * The number of #EXT-X-DISCONTINUITY tags in the playlist before the segment. */ @@ -52,16 +55,21 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * The start time of the segment in microseconds, relative to the start of the playlist. */ public final long relativeStartTimeUs; + /** + * DRM initialization data for sample decryption, or null if the segment does not use CDM-DRM + * protection. + */ + public final @Nullable DrmInitData drmInitData; /** * The encryption identity key uri as defined by #EXT-X-KEY, or null if the segment does not use * full segment encryption with identity key. */ - public final String fullSegmentEncryptionKeyUri; + public final @Nullable String fullSegmentEncryptionKeyUri; /** * The encryption initialization vector as defined by #EXT-X-KEY, or null if the segment is not * encrypted. */ - public final String encryptionIV; + public final @Nullable String encryptionIV; /** * The segment's byte range offset, as defined by #EXT-X-BYTERANGE. */ @@ -81,15 +89,29 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * @param byterangeLength See {@link #byterangeLength}. */ public Segment(String uri, long byterangeOffset, long byterangeLength) { - this(uri, null, 0, -1, C.TIME_UNSET, null, null, byterangeOffset, byterangeLength, false); + this( + uri, + /* initializationSegment= */ null, + /* title= */ "", + /* durationUs= */ 0, + /* relativeDiscontinuitySequence= */ -1, + /* relativeStartTimeUs= */ C.TIME_UNSET, + /* drmInitData= */ null, + /* fullSegmentEncryptionKeyUri= */ null, + /* encryptionIV= */ null, + byterangeOffset, + byterangeLength, + /* hasGapTag= */ false); } /** * @param url See {@link #url}. * @param initializationSegment See {@link #initializationSegment}. + * @param title See {@link #title}. * @param durationUs See {@link #durationUs}. * @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}. * @param relativeStartTimeUs See {@link #relativeStartTimeUs}. + * @param drmInitData See {@link #drmInitData}. * @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}. * @param encryptionIV See {@link #encryptionIV}. * @param byterangeOffset See {@link #byterangeOffset}. @@ -98,20 +120,24 @@ public final class HlsMediaPlaylist extends HlsPlaylist { */ public Segment( String url, - Segment initializationSegment, + @Nullable Segment initializationSegment, + String title, long durationUs, int relativeDiscontinuitySequence, long relativeStartTimeUs, - String fullSegmentEncryptionKeyUri, - String encryptionIV, + @Nullable DrmInitData drmInitData, + @Nullable String fullSegmentEncryptionKeyUri, + @Nullable String encryptionIV, long byterangeOffset, long byterangeLength, boolean hasGapTag) { this.url = url; this.initializationSegment = initializationSegment; + this.title = title; this.durationUs = durationUs; this.relativeDiscontinuitySequence = relativeDiscontinuitySequence; this.relativeStartTimeUs = relativeStartTimeUs; + this.drmInitData = drmInitData; this.fullSegmentEncryptionKeyUri = fullSegmentEncryptionKeyUri; this.encryptionIV = encryptionIV; this.byterangeOffset = byterangeOffset; @@ -173,10 +199,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * The target duration in microseconds, as defined by #EXT-X-TARGETDURATION. */ public final long targetDurationUs; - /** - * Whether the playlist contains the #EXT-X-INDEPENDENT-SEGMENTS tag. - */ - public final boolean hasIndependentSegmentsTag; /** * Whether the playlist contains the #EXT-X-ENDLIST tag. */ @@ -186,10 +208,10 @@ public final class HlsMediaPlaylist extends HlsPlaylist { */ public final boolean hasProgramDateTime; /** - * DRM initialization data for sample decryption, or null if none of the segment uses sample - * encryption. + * Contains the CDM protection schemes used by segments in this playlist. Does not contain any key + * acquisition data. Null if none of the segments in the playlist is CDM-encrypted. */ - public final DrmInitData drmInitData; + public final @Nullable DrmInitData protectionSchemes; /** * The list of segments in the playlist. */ @@ -210,10 +232,10 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * @param mediaSequence See {@link #mediaSequence}. * @param version See {@link #version}. * @param targetDurationUs See {@link #targetDurationUs}. - * @param hasIndependentSegmentsTag See {@link #hasIndependentSegmentsTag}. + * @param hasIndependentSegments See {@link #hasIndependentSegments}. * @param hasEndTag See {@link #hasEndTag}. + * @param protectionSchemes See {@link #protectionSchemes}. * @param hasProgramDateTime See {@link #hasProgramDateTime}. - * @param drmInitData See {@link #drmInitData}. * @param segments See {@link #segments}. */ public HlsMediaPlaylist( @@ -227,12 +249,12 @@ public final class HlsMediaPlaylist extends HlsPlaylist { long mediaSequence, int version, long targetDurationUs, - boolean hasIndependentSegmentsTag, + boolean hasIndependentSegments, boolean hasEndTag, boolean hasProgramDateTime, - DrmInitData drmInitData, + @Nullable DrmInitData protectionSchemes, List segments) { - super(baseUri, tags); + super(baseUri, tags, hasIndependentSegments); this.playlistType = playlistType; this.startTimeUs = startTimeUs; this.hasDiscontinuitySequence = hasDiscontinuitySequence; @@ -240,10 +262,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist { this.mediaSequence = mediaSequence; this.version = version; this.targetDurationUs = targetDurationUs; - this.hasIndependentSegmentsTag = hasIndependentSegmentsTag; this.hasEndTag = hasEndTag; this.hasProgramDateTime = hasProgramDateTime; - this.drmInitData = drmInitData; + this.protectionSchemes = protectionSchemes; this.segments = Collections.unmodifiableList(segments); if (!segments.isEmpty()) { Segment last = segments.get(segments.size() - 1); @@ -256,7 +277,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } @Override - public HlsMediaPlaylist copy(List renditionKeys) { + public HlsMediaPlaylist copy(List streamKeys) { return this; } @@ -294,7 +315,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * * @param startTimeUs The start time for the returned playlist. * @param discontinuitySequence The discontinuity sequence for the returned playlist. - * @return The playlist. + * @return An identical playlist including the provided discontinuity and timing information. */ public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) { return new HlsMediaPlaylist( @@ -308,18 +329,16 @@ public final class HlsMediaPlaylist extends HlsPlaylist { mediaSequence, version, targetDurationUs, - hasIndependentSegmentsTag, + hasIndependentSegments, hasEndTag, hasProgramDateTime, - drmInitData, + protectionSchemes, segments); } /** * Returns a playlist identical to this one except that an end tag is added. If an end tag is * already present then the playlist will return itself. - * - * @return The playlist. */ public HlsMediaPlaylist copyWithEndTag() { if (this.hasEndTag) { @@ -336,10 +355,10 @@ public final class HlsMediaPlaylist extends HlsPlaylist { mediaSequence, version, targetDurationUs, - hasIndependentSegmentsTag, + hasIndependentSegments, /* hasEndTag= */ true, hasProgramDateTime, - drmInitData, + protectionSchemes, segments); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistSegmentIterator.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistSegmentIterator.java new file mode 100644 index 0000000000..4c654dc572 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistSegmentIterator.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.hls.playlist; + +import android.net.Uri; +import com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator; +import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.util.UriUtil; + +/** {@link MediaChunkIterator} wrapping a {@link HlsMediaPlaylist}. */ +public final class HlsMediaPlaylistSegmentIterator extends BaseMediaChunkIterator { + + private final HlsMediaPlaylist playlist; + private final long startOfPlaylistInPeriodUs; + + /** + * Creates iterator. + * + * @param playlist The {@link HlsMediaPlaylist} to wrap. + * @param startOfPlaylistInPeriodUs The start time of the playlist in the period, in microseconds. + * @param chunkIndex The chunk index in the playlist at which the iterator will start. + */ + public HlsMediaPlaylistSegmentIterator( + HlsMediaPlaylist playlist, long startOfPlaylistInPeriodUs, int chunkIndex) { + super(/* fromIndex= */ chunkIndex, /* toIndex= */ playlist.segments.size() - 1); + this.playlist = playlist; + this.startOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs; + } + + @Override + public DataSpec getDataSpec() { + checkInBounds(); + HlsMediaPlaylist.Segment segment = playlist.segments.get((int) getCurrentIndex()); + Uri chunkUri = UriUtil.resolveToUri(playlist.baseUri, segment.url); + return new DataSpec( + chunkUri, segment.byterangeOffset, segment.byterangeLength, /* key= */ null); + } + + @Override + public long getChunkStartTimeUs() { + checkInBounds(); + HlsMediaPlaylist.Segment segment = playlist.segments.get((int) getCurrentIndex()); + return startOfPlaylistInPeriodUs + segment.relativeStartTimeUs; + } + + @Override + public long getChunkEndTimeUs() { + checkInBounds(); + HlsMediaPlaylist.Segment segment = playlist.segments.get((int) getCurrentIndex()); + long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + segment.relativeStartTimeUs; + return segmentStartTimeInPeriodUs + segment.durationUs; + } +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java index 34ecde229d..9cec1cd33b 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java @@ -20,7 +20,7 @@ import java.util.Collections; import java.util.List; /** Represents an HLS playlist. */ -public abstract class HlsPlaylist implements FilterableManifest { +public abstract class HlsPlaylist implements FilterableManifest { /** * The base uri. Used to resolve relative paths. @@ -30,14 +30,21 @@ public abstract class HlsPlaylist implements FilterableManifest tags; + /** + * Whether the media is formed of independent segments, as defined by the + * #EXT-X-INDEPENDENT-SEGMENTS tag. + */ + public final boolean hasIndependentSegments; /** * @param baseUri See {@link #baseUri}. * @param tags See {@link #tags}. + * @param hasIndependentSegments See {@link #hasIndependentSegments}. */ - protected HlsPlaylist(String baseUri, List tags) { + protected HlsPlaylist(String baseUri, List tags, boolean hasIndependentSegments) { this.baseUri = baseUri; this.tags = Collections.unmodifiableList(tags); + this.hasIndependentSegments = hasIndependentSegments; } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 7187bdb0ca..e287b5220e 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -16,12 +16,14 @@ package com.google.android.exoplayer2.source.hls.playlist; import android.net.Uri; +import android.support.annotation.Nullable; import android.util.Base64; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.source.UnrecognizedInputFormatException; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import com.google.android.exoplayer2.upstream.ParsingLoadable; @@ -39,8 +41,10 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Queue; +import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.PolyNull; /** * HLS playlists parsing logic. @@ -80,6 +84,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser muxedCaptionFormats = null; boolean noClosedCaptions = false; + boolean hasIndependentSegmentsTag = false; String line; while (iterator.hasNext()) { @@ -227,7 +258,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser(); } - muxedCaptionFormats.add(Format.createTextContainerFormat(id, null, mimeType, null, - Format.NO_VALUE, selectionFlags, language, accessibilityChannel)); + muxedCaptionFormats.add( + Format.createTextContainerFormat( + /* id= */ name, + /* label= */ name, + /* containerMimeType= */ null, + /* sampleMimeType= */ mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + selectionFlags, + language, + accessibilityChannel)); break; default: // Do nothing. @@ -326,31 +397,47 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser segments = new ArrayList<>(); List tags = new ArrayList<>(); long segmentDurationUs = 0; + String segmentTitle = ""; boolean hasDiscontinuitySequence = false; int playlistDiscontinuitySequence = 0; int relativeDiscontinuitySequence = 0; @@ -361,9 +448,12 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser currentSchemeDatas = new TreeMap<>(); + String encryptionScheme = null; + DrmInitData cachedDrmInitData = null; String line; while (iterator.hasNext()) { @@ -406,14 +496,18 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser createPlaylistParser() { + return new HlsPlaylistParser(); + } + + @Override + public ParsingLoadable.Parser createPlaylistParser( + HlsMasterPlaylist masterPlaylist) { + return new HlsPlaylistParser(masterPlaylist); + } + }; + + /** + * Returns a stand-alone playlist parser. Playlists parsed by the returned parser do not inherit + * any attributes from other playlists. + */ + ParsingLoadable.Parser createPlaylistParser(); + + /** + * Returns a playlist parser for playlists that were referenced by the given {@link + * HlsMasterPlaylist}. Returned {@link HlsMediaPlaylist} instances may inherit attributes from + * {@code masterPlaylist}. + * + * @param masterPlaylist The master playlist that referenced any parsed media playlists. + * @return A parser for HLS playlists. + */ + ParsingLoadable.Parser createPlaylistParser(HlsMasterPlaylist masterPlaylist); +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index 9986f5b65b..49896bd57b 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,66 +16,28 @@ package com.google.android.exoplayer2.source.hls.playlist; import android.net.Uri; -import android.os.Handler; -import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; -import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; -import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; -import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.Loader; -import com.google.android.exoplayer2.upstream.ParsingLoadable; -import com.google.android.exoplayer2.util.UriUtil; import java.io.IOException; -import java.util.ArrayList; -import java.util.IdentityHashMap; -import java.util.List; /** - * Tracks playlists linked to a provided playlist url. The provided url might reference an HLS - * master playlist or a media playlist. + * Tracks playlists associated to an HLS stream and provides snapshots. + * + *

    The playlist tracker is responsible for exposing the seeking window, which is defined by the + * segments that one of the playlists exposes. This playlist is called primary and needs to be + * periodically refreshed in the case of live streams. Note that the primary playlist is one of the + * media playlists while the master playlist is an optional kind of playlist defined by the HLS + * specification (RFC 8216). + * + *

    Playlist loads might encounter errors. The tracker may choose to blacklist them to ensure a + * primary playlist is always available. */ -public final class HlsPlaylistTracker implements Loader.Callback> { +public interface HlsPlaylistTracker { - /** - * Thrown when a playlist is considered to be stuck due to a server side error. - */ - public static final class PlaylistStuckException extends IOException { - - /** - * The url of the stuck playlist. - */ - public final String url; - - private PlaylistStuckException(String url) { - this.url = url; - } - - } - - /** - * Thrown when the media sequence of a new snapshot indicates the server has reset. - */ - public static final class PlaylistResetException extends IOException { - - /** - * The url of the reset playlist. - */ - public final String url; - - private PlaylistResetException(String url) { - this.url = url; - } - - } - - /** - * Listener for primary playlist changes. - */ - public interface PrimaryPlaylistListener { + /** Listener for primary playlist changes. */ + interface PrimaryPlaylistListener { /** * Called when the primary playlist changes. @@ -85,10 +47,8 @@ public final class HlsPlaylistTracker implements Loader.Callback playlistParser; - private final int minRetryCount; - private final IdentityHashMap playlistBundles; - private final Handler playlistRefreshHandler; - private final PrimaryPlaylistListener primaryPlaylistListener; - private final List listeners; - private final Loader initialPlaylistLoader; - private final EventDispatcher eventDispatcher; - - private HlsMasterPlaylist masterPlaylist; - private HlsUrl primaryHlsUrl; - private HlsMediaPlaylist primaryUrlSnapshot; - private boolean isLive; - private long initialStartTimeUs; - - /** - * @param initialPlaylistUri Uri for the initial playlist of the stream. Can refer a media - * playlist or a master playlist. - * @param dataSourceFactory A factory for {@link DataSource} instances. + * Starts the playlist tracker. + * + *

    Must be called from the playback thread. A tracker may be restarted after a {@link #stop()} + * call. + * + * @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a master + * playlist. * @param eventDispatcher A dispatcher to notify of events. - * @param minRetryCount The minimum number of times loads must be retried before - * {@link #maybeThrowPlaylistRefreshError(HlsUrl)} and - * {@link #maybeThrowPrimaryPlaylistRefreshError()} propagate any loading errors. - * @param primaryPlaylistListener A callback for the primary playlist change events. + * @param listener A callback for the primary playlist change events. */ - public HlsPlaylistTracker(Uri initialPlaylistUri, HlsDataSourceFactory dataSourceFactory, - EventDispatcher eventDispatcher, int minRetryCount, - PrimaryPlaylistListener primaryPlaylistListener, - ParsingLoadable.Parser playlistParser) { - this.initialPlaylistUri = initialPlaylistUri; - this.dataSourceFactory = dataSourceFactory; - this.eventDispatcher = eventDispatcher; - this.minRetryCount = minRetryCount; - this.primaryPlaylistListener = primaryPlaylistListener; - this.playlistParser = playlistParser; - listeners = new ArrayList<>(); - initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist"); - playlistBundles = new IdentityHashMap<>(); - playlistRefreshHandler = new Handler(); - initialStartTimeUs = C.TIME_UNSET; - } + void start( + Uri initialPlaylistUri, EventDispatcher eventDispatcher, PrimaryPlaylistListener listener); + + /** + * Stops the playlist tracker and releases any acquired resources. + * + *

    Must be called once per {@link #start} call. + */ + void stop(); /** * Registers a listener to receive events from the playlist tracker. * * @param listener The listener. */ - public void addListener(PlaylistEventListener listener) { - listeners.add(listener); - } + void addListener(PlaylistEventListener listener); /** * Unregisters a listener. * * @param listener The listener to unregister. */ - public void removeListener(PlaylistEventListener listener) { - listeners.remove(listener); - } - - /** - * Starts tracking all the playlists related to the provided Uri. - */ - public void start() { - ParsingLoadable masterPlaylistLoadable = new ParsingLoadable<>( - dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), initialPlaylistUri, - C.DATA_TYPE_MANIFEST, playlistParser); - initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount); - } + void removeListener(PlaylistEventListener listener); /** * Returns the master playlist. * + *

    If the uri passed to {@link #start} points to a media playlist, an {@link HlsMasterPlaylist} + * with a single variant for said media playlist is returned. + * * @return The master playlist. Null if the initial playlist has yet to be loaded. */ - public HlsMasterPlaylist getMasterPlaylist() { - return masterPlaylist; - } + @Nullable + HlsMasterPlaylist getMasterPlaylist(); /** - * Returns the most recent snapshot available of the playlist referenced by the provided - * {@link HlsUrl}. + * Returns the most recent snapshot available of the playlist referenced by the provided {@link + * HlsUrl}. * * @param url The {@link HlsUrl} corresponding to the requested media playlist. * @return The most recent snapshot of the playlist referenced by the provided {@link HlsUrl}. May * be null if no snapshot has been loaded yet. */ - public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) { - HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot(); - if (snapshot != null) { - maybeSetPrimaryUrl(url); - } - return snapshot; - } + @Nullable + HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url); /** * Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no * media playlist has been loaded. */ - public long getInitialStartTimeUs() { - return initialStartTimeUs; - } + long getInitialStartTimeUs(); /** * Returns whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is * valid, meaning all the segments referenced by the playlist are expected to be available. If the * playlist is not valid then some of the segments may no longer be available. - + * * @param url The {@link HlsUrl}. * @return Whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is * valid. */ - public boolean isSnapshotValid(HlsUrl url) { - return playlistBundles.get(url).isSnapshotValid(); - } - - /** - * Releases the playlist tracker. - */ - public void release() { - initialPlaylistLoader.release(); - for (MediaPlaylistBundle bundle : playlistBundles.values()) { - bundle.release(); - } - playlistRefreshHandler.removeCallbacksAndMessages(null); - playlistBundles.clear(); - } + boolean isSnapshotValid(HlsUrl url); /** * If the tracker is having trouble refreshing the master playlist or the primary playlist, this @@ -247,401 +178,31 @@ public final class HlsPlaylistTracker implements Loader.CallbackThe playlist tracker may choose the delay the playlist refresh. The request is discarded if + * a refresh was already pending. * * @param url The {@link HlsUrl} of the playlist to be refreshed. */ - public void refreshPlaylist(HlsUrl url) { - playlistBundles.get(url).loadPlaylist(); - } + void refreshPlaylist(HlsUrl url); /** - * Returns whether this is live content. + * Returns whether the tracked playlists describe a live stream. * * @return True if the content is live. False otherwise. */ - public boolean isLive() { - return isLive; - } - - // Loader.Callback implementation. - - @Override - public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { - HlsPlaylist result = loadable.getResult(); - HlsMasterPlaylist masterPlaylist; - boolean isMediaPlaylist = result instanceof HlsMediaPlaylist; - if (isMediaPlaylist) { - masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri); - } else /* result instanceof HlsMasterPlaylist */ { - masterPlaylist = (HlsMasterPlaylist) result; - } - this.masterPlaylist = masterPlaylist; - primaryHlsUrl = masterPlaylist.variants.get(0); - ArrayList urls = new ArrayList<>(); - urls.addAll(masterPlaylist.variants); - urls.addAll(masterPlaylist.audios); - urls.addAll(masterPlaylist.subtitles); - createBundles(urls); - MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryHlsUrl); - if (isMediaPlaylist) { - // We don't need to load the playlist again. We can use the same result. - primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result); - } else { - primaryBundle.loadPlaylist(); - } - eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } - - @Override - public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } - - @Override - public @Loader.RetryAction int onLoadError( - ParsingLoadable loadable, - long elapsedRealtimeMs, - long loadDurationMs, - IOException error) { - boolean isFatal = error instanceof ParserException; - eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded(), error, isFatal); - return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; - } - - // Internal methods. - - private boolean maybeSelectNewPrimaryUrl() { - List variants = masterPlaylist.variants; - int variantsSize = variants.size(); - long currentTimeMs = SystemClock.elapsedRealtime(); - for (int i = 0; i < variantsSize; i++) { - MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i)); - if (currentTimeMs > bundle.blacklistUntilMs) { - primaryHlsUrl = bundle.playlistUrl; - bundle.loadPlaylist(); - return true; - } - } - return false; - } - - private void maybeSetPrimaryUrl(HlsUrl url) { - if (url == primaryHlsUrl - || !masterPlaylist.variants.contains(url) - || (primaryUrlSnapshot != null && primaryUrlSnapshot.hasEndTag)) { - // Ignore if the primary url is unchanged, if the url is not a variant url, or if the last - // primary snapshot contains an end tag. - return; - } - primaryHlsUrl = url; - playlistBundles.get(primaryHlsUrl).loadPlaylist(); - } - - private void createBundles(List urls) { - int listSize = urls.size(); - for (int i = 0; i < listSize; i++) { - HlsUrl url = urls.get(i); - MediaPlaylistBundle bundle = new MediaPlaylistBundle(url); - playlistBundles.put(url, bundle); - } - } - - /** - * Called by the bundles when a snapshot changes. - * - * @param url The url of the playlist. - * @param newSnapshot The new snapshot. - */ - private void onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot) { - if (url == primaryHlsUrl) { - if (primaryUrlSnapshot == null) { - // This is the first primary url snapshot. - isLive = !newSnapshot.hasEndTag; - initialStartTimeUs = newSnapshot.startTimeUs; - } - primaryUrlSnapshot = newSnapshot; - primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot); - } - int listenersSize = listeners.size(); - for (int i = 0; i < listenersSize; i++) { - listeners.get(i).onPlaylistChanged(); - } - } - - private boolean notifyPlaylistError(HlsUrl playlistUrl, boolean shouldBlacklist) { - int listenersSize = listeners.size(); - boolean anyBlacklistingFailed = false; - for (int i = 0; i < listenersSize; i++) { - anyBlacklistingFailed |= !listeners.get(i).onPlaylistError(playlistUrl, shouldBlacklist); - } - return anyBlacklistingFailed; - } - - private HlsMediaPlaylist getLatestPlaylistSnapshot(HlsMediaPlaylist oldPlaylist, - HlsMediaPlaylist loadedPlaylist) { - if (!loadedPlaylist.isNewerThan(oldPlaylist)) { - if (loadedPlaylist.hasEndTag) { - // If the loaded playlist has an end tag but is not newer than the old playlist then we have - // an inconsistent state. This is typically caused by the server incorrectly resetting the - // media sequence when appending the end tag. We resolve this case as best we can by - // returning the old playlist with the end tag appended. - return oldPlaylist.copyWithEndTag(); - } else { - return oldPlaylist; - } - } - long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist); - int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist); - return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence); - } - - private long getLoadedPlaylistStartTimeUs(HlsMediaPlaylist oldPlaylist, - HlsMediaPlaylist loadedPlaylist) { - if (loadedPlaylist.hasProgramDateTime) { - return loadedPlaylist.startTimeUs; - } - long primarySnapshotStartTimeUs = primaryUrlSnapshot != null - ? primaryUrlSnapshot.startTimeUs : 0; - if (oldPlaylist == null) { - return primarySnapshotStartTimeUs; - } - int oldPlaylistSize = oldPlaylist.segments.size(); - Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); - if (firstOldOverlappingSegment != null) { - return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs; - } else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) { - return oldPlaylist.getEndTimeUs(); - } else { - // No segments overlap, we assume the new playlist start coincides with the primary playlist. - return primarySnapshotStartTimeUs; - } - } - - private int getLoadedPlaylistDiscontinuitySequence(HlsMediaPlaylist oldPlaylist, - HlsMediaPlaylist loadedPlaylist) { - if (loadedPlaylist.hasDiscontinuitySequence) { - return loadedPlaylist.discontinuitySequence; - } - // TODO: Improve cross-playlist discontinuity adjustment. - int primaryUrlDiscontinuitySequence = primaryUrlSnapshot != null - ? primaryUrlSnapshot.discontinuitySequence : 0; - if (oldPlaylist == null) { - return primaryUrlDiscontinuitySequence; - } - Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); - if (firstOldOverlappingSegment != null) { - return oldPlaylist.discontinuitySequence - + firstOldOverlappingSegment.relativeDiscontinuitySequence - - loadedPlaylist.segments.get(0).relativeDiscontinuitySequence; - } - return primaryUrlDiscontinuitySequence; - } - - private static Segment getFirstOldOverlappingSegment(HlsMediaPlaylist oldPlaylist, - HlsMediaPlaylist loadedPlaylist) { - int mediaSequenceOffset = (int) (loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence); - List oldSegments = oldPlaylist.segments; - return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null; - } - - /** - * Holds all information related to a specific Media Playlist. - */ - private final class MediaPlaylistBundle implements Loader.Callback>, - Runnable { - - private final HlsUrl playlistUrl; - private final Loader mediaPlaylistLoader; - private final ParsingLoadable mediaPlaylistLoadable; - - private HlsMediaPlaylist playlistSnapshot; - private long lastSnapshotLoadMs; - private long lastSnapshotChangeMs; - private long earliestNextLoadTimeMs; - private long blacklistUntilMs; - private boolean loadPending; - private IOException playlistError; - - public MediaPlaylistBundle(HlsUrl playlistUrl) { - this.playlistUrl = playlistUrl; - mediaPlaylistLoader = new Loader("HlsPlaylistTracker:MediaPlaylist"); - mediaPlaylistLoadable = new ParsingLoadable<>( - dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), - UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST, - playlistParser); - } - - public HlsMediaPlaylist getPlaylistSnapshot() { - return playlistSnapshot; - } - - public boolean isSnapshotValid() { - if (playlistSnapshot == null) { - return false; - } - long currentTimeMs = SystemClock.elapsedRealtime(); - long snapshotValidityDurationMs = Math.max(30000, C.usToMs(playlistSnapshot.durationUs)); - return playlistSnapshot.hasEndTag - || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT - || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD - || lastSnapshotLoadMs + snapshotValidityDurationMs > currentTimeMs; - } - - public void release() { - mediaPlaylistLoader.release(); - } - - public void loadPlaylist() { - blacklistUntilMs = 0; - if (loadPending || mediaPlaylistLoader.isLoading()) { - // Load already pending or in progress. Do nothing. - return; - } - long currentTimeMs = SystemClock.elapsedRealtime(); - if (currentTimeMs < earliestNextLoadTimeMs) { - loadPending = true; - playlistRefreshHandler.postDelayed(this, earliestNextLoadTimeMs - currentTimeMs); - } else { - loadPlaylistImmediately(); - } - } - - public void maybeThrowPlaylistRefreshError() throws IOException { - mediaPlaylistLoader.maybeThrowError(); - if (playlistError != null) { - throw playlistError; - } - } - - // Loader.Callback implementation. - - @Override - public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { - HlsPlaylist result = loadable.getResult(); - if (result instanceof HlsMediaPlaylist) { - processLoadedPlaylist((HlsMediaPlaylist) result); - eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } else { - playlistError = new ParserException("Loaded playlist has unexpected type."); - } - } - - @Override - public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } - - @Override - public @Loader.RetryAction int onLoadError( - ParsingLoadable loadable, - long elapsedRealtimeMs, - long loadDurationMs, - IOException error) { - boolean isFatal = error instanceof ParserException; - eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded(), error, isFatal); - boolean shouldBlacklist = ChunkedTrackBlacklistUtil.shouldBlacklist(error); - boolean shouldRetryIfNotFatal = - notifyPlaylistError(playlistUrl, shouldBlacklist) || !shouldBlacklist; - if (isFatal) { - return Loader.DONT_RETRY_FATAL; - } - if (shouldBlacklist) { - shouldRetryIfNotFatal |= blacklistPlaylist(); - } - return shouldRetryIfNotFatal ? Loader.RETRY : Loader.DONT_RETRY; - } - - // Runnable implementation. - - @Override - public void run() { - loadPending = false; - loadPlaylistImmediately(); - } - - // Internal methods. - - private void loadPlaylistImmediately() { - mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount); - } - - private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist) { - HlsMediaPlaylist oldPlaylist = playlistSnapshot; - long currentTimeMs = SystemClock.elapsedRealtime(); - lastSnapshotLoadMs = currentTimeMs; - playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist); - if (playlistSnapshot != oldPlaylist) { - playlistError = null; - lastSnapshotChangeMs = currentTimeMs; - onPlaylistUpdated(playlistUrl, playlistSnapshot); - } else if (!playlistSnapshot.hasEndTag) { - if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size() - < playlistSnapshot.mediaSequence) { - // The media sequence jumped backwards. The server has probably reset. - playlistError = new PlaylistResetException(playlistUrl.url); - notifyPlaylistError(playlistUrl, false); - } else if (currentTimeMs - lastSnapshotChangeMs - > C.usToMs(playlistSnapshot.targetDurationUs) - * PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) { - // The playlist seems to be stuck. Blacklist it. - playlistError = new PlaylistStuckException(playlistUrl.url); - notifyPlaylistError(playlistUrl, true); - blacklistPlaylist(); - } - } - // Do not allow the playlist to load again within the target duration if we obtained a new - // snapshot, or half the target duration otherwise. - earliestNextLoadTimeMs = currentTimeMs + C.usToMs(playlistSnapshot != oldPlaylist - ? playlistSnapshot.targetDurationUs : (playlistSnapshot.targetDurationUs / 2)); - // Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the - // next load will be scheduled when refreshPlaylist is called, or when this playlist becomes - // the primary. - if (playlistUrl == primaryHlsUrl && !playlistSnapshot.hasEndTag) { - loadPlaylist(); - } - } - - /** - * Blacklists the playlist. - * - * @return Whether the playlist is the primary, despite being blacklisted. - */ - private boolean blacklistPlaylist() { - blacklistUntilMs = SystemClock.elapsedRealtime() - + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS; - return primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl(); - } - - } - + boolean isLive(); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/RenditionKey.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/RenditionKey.java deleted file mode 100644 index dec5882efb..0000000000 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/RenditionKey.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.source.hls.playlist; - -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** Uniquely identifies a rendition in an {@link HlsMasterPlaylist}. */ -public final class RenditionKey implements Comparable { - - /** Types of rendition. */ - @Retention(RetentionPolicy.SOURCE) - @IntDef({TYPE_VARIANT, TYPE_AUDIO, TYPE_SUBTITLE}) - public @interface Type {} - - public static final int TYPE_VARIANT = 0; - public static final int TYPE_AUDIO = 1; - public static final int TYPE_SUBTITLE = 2; - - public final @Type int type; - public final int trackIndex; - - public RenditionKey(@Type int type, int trackIndex) { - this.type = type; - this.trackIndex = trackIndex; - } - - @Override - public String toString() { - return type + "." + trackIndex; - } - - @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - RenditionKey that = (RenditionKey) o; - return type == that.type && trackIndex == that.trackIndex; - } - - @Override - public int hashCode() { - int result = type; - result = 31 * result + trackIndex; - return result; - } - - // Comparable implementation. - - @Override - public int compareTo(@NonNull RenditionKey other) { - int result = type - other.type; - if (result == 0) { - result = trackIndex - other.trackIndex; - } - return result; - } -} diff --git a/library/hls/src/main/proguard-rules.txt b/library/hls/src/main/proguard-rules.txt new file mode 100644 index 0000000000..3b8d1bb4ac --- /dev/null +++ b/library/hls/src/main/proguard-rules.txt @@ -0,0 +1,7 @@ +# Proguard rules specific to the hls module. + +# Constructors accessed via reflection in SegmentDownloadAction +-dontnote com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction +-keepclassmembers class com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction { + static ** DESERIALIZER; +} diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/Aes128DataSourceTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/Aes128DataSourceTest.java index 86bffc7762..defc838c6a 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/Aes128DataSourceTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/Aes128DataSourceTest.java @@ -21,7 +21,11 @@ import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.TransferListener; import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -33,7 +37,7 @@ public class Aes128DataSourceTest { @Test public void test_OpenCallsUpstreamOpen_CloseCallsUpstreamClose() throws IOException { UpstreamDataSource upstream = new UpstreamDataSource(); - Aes128DataSource testInstance = new Aes128DataSource(upstream, new byte[16], new byte[16]); + Aes128DataSource testInstance = new TestAes123DataSource(upstream, new byte[16], new byte[16]); assertThat(upstream.opened).isFalse(); Uri uri = Uri.parse("http.abc.com/def"); @@ -53,7 +57,7 @@ public class Aes128DataSourceTest { throw new IOException(); } }; - Aes128DataSource testInstance = new Aes128DataSource(upstream, new byte[16], new byte[16]); + Aes128DataSource testInstance = new TestAes123DataSource(upstream, new byte[16], new byte[16]); assertThat(upstream.opened).isFalse(); Uri uri = Uri.parse("http.abc.com/def"); @@ -71,11 +75,33 @@ public class Aes128DataSourceTest { assertThat(upstream.closedCalled).isTrue(); } + private static class TestAes123DataSource extends Aes128DataSource { + + public TestAes123DataSource(DataSource upstream, byte[] encryptionKey, byte[] encryptionIv) { + super(upstream, encryptionKey, encryptionIv); + } + + @Override + protected Cipher getCipherInstance() throws NoSuchPaddingException, NoSuchAlgorithmException { + try { + return super.getCipherInstance(); + } catch (NoSuchAlgorithmException e) { + // Some host machines may not provide an algorithm for "AES/CBC/PKCS7Padding", however on + // such machines it's possible to get a functionally identical algorithm by requesting + // "AES/CBC/PKCS5Padding". + return Cipher.getInstance("AES/CBC/PKCS5Padding"); + } + } + } + private static class UpstreamDataSource implements DataSource { public boolean opened; public boolean closedCalled; + @Override + public void addTransferListener(TransferListener transferListener) {} + @Override public long open(DataSpec dataSpec) throws IOException { opened = true; diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadActionTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadActionTest.java new file mode 100644 index 0000000000..778ecadddd --- /dev/null +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadActionTest.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.hls.offline; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +/** Unit tests for {@link HlsDownloadAction}. */ +@RunWith(RobolectricTestRunner.class) +public class HlsDownloadActionTest { + + private Uri uri1; + private Uri uri2; + + @Before + public void setUp() { + uri1 = Uri.parse("http://test1.uri"); + uri2 = Uri.parse("http://test2.uri"); + } + + @Test + public void testDownloadActionIsNotRemoveAction() { + DownloadAction action = createDownloadAction(uri1); + assertThat(action.isRemoveAction).isFalse(); + } + + @Test + public void testRemoveActionIsRemoveAction() { + DownloadAction action2 = createRemoveAction(uri1); + assertThat(action2.isRemoveAction).isTrue(); + } + + @Test + public void testCreateDownloader() { + MockitoAnnotations.initMocks(this); + DownloadAction action = createDownloadAction(uri1); + DownloaderConstructorHelper constructorHelper = + new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); + assertThat(action.createDownloader(constructorHelper)).isNotNull(); + } + + @Test + public void testSameUriDifferentAction_IsSameMedia() { + DownloadAction action1 = createRemoveAction(uri1); + DownloadAction action2 = createDownloadAction(uri1); + assertThat(action1.isSameMedia(action2)).isTrue(); + } + + @Test + public void testDifferentUriAndAction_IsNotSameMedia() { + DownloadAction action3 = createRemoveAction(uri2); + DownloadAction action4 = createDownloadAction(uri1); + assertThat(action3.isSameMedia(action4)).isFalse(); + } + + @SuppressWarnings("EqualsWithItself") + @Test + public void testEquals() { + DownloadAction action1 = createRemoveAction(uri1); + assertThat(action1.equals(action1)).isTrue(); + + DownloadAction action2 = createRemoveAction(uri1); + DownloadAction action3 = createRemoveAction(uri1); + assertEqual(action2, action3); + + DownloadAction action4 = createRemoveAction(uri1); + DownloadAction action5 = createDownloadAction(uri1); + assertNotEqual(action4, action5); + + DownloadAction action6 = createDownloadAction(uri1); + DownloadAction action7 = createDownloadAction(uri1, new StreamKey(0, 0)); + assertNotEqual(action6, action7); + + DownloadAction action8 = createDownloadAction(uri1, new StreamKey(1, 1)); + DownloadAction action9 = createDownloadAction(uri1, new StreamKey(0, 0)); + assertNotEqual(action8, action9); + + DownloadAction action10 = createRemoveAction(uri1); + DownloadAction action11 = createRemoveAction(uri2); + assertNotEqual(action10, action11); + + DownloadAction action12 = createDownloadAction(uri1, new StreamKey(0, 0), new StreamKey(1, 1)); + DownloadAction action13 = createDownloadAction(uri1, new StreamKey(1, 1), new StreamKey(0, 0)); + assertEqual(action12, action13); + + DownloadAction action14 = createDownloadAction(uri1, new StreamKey(0, 0)); + DownloadAction action15 = createDownloadAction(uri1, new StreamKey(1, 1), new StreamKey(0, 0)); + assertNotEqual(action14, action15); + + DownloadAction action16 = createDownloadAction(uri1); + DownloadAction action17 = createDownloadAction(uri1); + assertEqual(action16, action17); + } + + @Test + public void testSerializerGetType() { + DownloadAction action = createDownloadAction(uri1); + assertThat(action.type).isNotNull(); + } + + @Test + public void testSerializerWriteRead() throws Exception { + doTestSerializationRoundTrip(createDownloadAction(uri1)); + doTestSerializationRoundTrip(createRemoveAction(uri1)); + doTestSerializationRoundTrip( + createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1))); + } + + @Test + public void testSerializerVersion0() throws Exception { + doTestSerializationV0RoundTrip(createDownloadAction(uri1)); + doTestSerializationV0RoundTrip(createRemoveAction(uri1)); + doTestSerializationV0RoundTrip( + createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1))); + } + + private static void assertNotEqual(DownloadAction action1, DownloadAction action2) { + assertThat(action1).isNotEqualTo(action2); + assertThat(action2).isNotEqualTo(action1); + } + + private static void assertEqual(DownloadAction action1, DownloadAction action2) { + assertThat(action1).isEqualTo(action2); + assertThat(action2).isEqualTo(action1); + } + + private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(out); + DownloadAction.serializeToStream(action, output); + + assertEqual(action, deserializeActionFromStream(out)); + } + + private static void doTestSerializationV0RoundTrip(HlsDownloadAction action) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(out); + DataOutputStream dataOutputStream = new DataOutputStream(output); + dataOutputStream.writeUTF(action.type); + dataOutputStream.writeInt(/* version */ 0); + dataOutputStream.writeUTF(action.uri.toString()); + dataOutputStream.writeBoolean(action.isRemoveAction); + dataOutputStream.writeInt(action.data.length); + dataOutputStream.write(action.data); + dataOutputStream.writeInt(action.keys.size()); + for (int i = 0; i < action.keys.size(); i++) { + StreamKey key = action.keys.get(i); + dataOutputStream.writeInt(key.groupIndex); + dataOutputStream.writeInt(key.trackIndex); + } + dataOutputStream.flush(); + + assertEqual(action, deserializeActionFromStream(out)); + } + + private static DownloadAction deserializeActionFromStream(ByteArrayOutputStream out) + throws IOException { + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + DataInputStream input = new DataInputStream(in); + return DownloadAction.deserializeFromStream( + new DownloadAction.Deserializer[] {HlsDownloadAction.DESERIALIZER}, input); + } + + private static HlsDownloadAction createDownloadAction(Uri uri, StreamKey... keys) { + ArrayList keysList = new ArrayList<>(); + Collections.addAll(keysList, keys); + return HlsDownloadAction.createDownloadAction(uri, null, keysList); + } + + private static HlsDownloadAction createRemoveAction(Uri uri) { + return HlsDownloadAction.createRemoveAction(uri, null); + } +} diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java index 6e816dd8a7..acc5236311 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java @@ -36,7 +36,8 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.source.hls.playlist.RenditionKey; +import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; @@ -181,17 +182,18 @@ public class HlsDownloaderTest { assertCachedData(cache, fakeDataSet); } - private HlsDownloader getHlsDownloader(String mediaPlaylistUri, List keys) { + private HlsDownloader getHlsDownloader(String mediaPlaylistUri, List keys) { Factory factory = new Factory(null).setFakeDataSet(fakeDataSet); return new HlsDownloader( Uri.parse(mediaPlaylistUri), keys, new DownloaderConstructorHelper(cache, factory)); } - private static ArrayList getKeys(int... variantIndices) { - ArrayList renditionKeys = new ArrayList<>(); + private static ArrayList getKeys(int... variantIndices) { + ArrayList streamKeys = new ArrayList<>(); for (int variantIndex : variantIndices) { - renditionKeys.add(new RenditionKey(RenditionKey.TYPE_VARIANT, variantIndex)); + final int trackIndex = variantIndex; + streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, trackIndex)); } - return renditionKeys; + return streamKeys; } } diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index 86426e1f94..11fef3c844 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -105,6 +105,18 @@ public class HlsMasterPlaylistParserTest { + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud2\",LANGUAGE=\"en\",NAME=\"English\"," + "AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"6\",URI=\"a2/prog_index.m3u8\"\n"; + private static final String PLAYLIST_WITH_INDEPENDENT_SEGMENTS = + " #EXTM3U\n" + + "\n" + + "#EXT-X-INDEPENDENT-SEGMENTS\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000," + + "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + + "http://example.com/low.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" + + "http://example.com/spaces_in_codecs.m3u8\n"; + @Test public void testParseMasterPlaylist() throws IOException { HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); @@ -195,6 +207,17 @@ public class HlsMasterPlaylistParserTest { assertThat(secondAudioFormat.sampleMimeType).isEqualTo(MimeTypes.AUDIO_AC3); } + @Test + public void testIndependentSegments() throws IOException { + HlsMasterPlaylist playlistWithIndependentSegments = + parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INDEPENDENT_SEGMENTS); + assertThat(playlistWithIndependentSegments.hasIndependentSegments).isTrue(); + + HlsMasterPlaylist playlistWithoutIndependentSegments = + parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); + assertThat(playlistWithoutIndependentSegments.hasIndependentSegments).isFalse(); + } + private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) throws IOException { Uri playlistUri = Uri.parse(uri); diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index 7a8a4d7925..6e71aebb74 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -24,7 +24,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.Charset; +import java.util.Collections; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; @@ -53,26 +53,25 @@ public class HlsMediaPlaylistParserTest { + "\n" + "#EXT-X-KEY:METHOD=AES-128," + "URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n" - + "#EXTINF:7.975,\n" + + "#EXTINF:7.975,segment title\n" + "#EXT-X-BYTERANGE:51501@2147483648\n" + "https://priv.example.com/fileSequence2680.ts\n" + "\n" + "#EXT-X-KEY:METHOD=NONE\n" - + "#EXTINF:7.941,\n" + + "#EXTINF:7.941,segment title .,:/# with interesting chars\n" + "#EXT-X-BYTERANGE:51501\n" // @2147535149 + "https://priv.example.com/fileSequence2681.ts\n" + "\n" + "#EXT-X-DISCONTINUITY\n" + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2682\"\n" - + "#EXTINF:7.975,\n" + + "#EXTINF:7.975\n" // Trailing comma is omitted. + "#EXT-X-BYTERANGE:51740\n" // @2147586650 + "https://priv.example.com/fileSequence2682.ts\n" + "\n" + "#EXTINF:7.975,\n" + "https://priv.example.com/fileSequence2683.ts\n" + "#EXT-X-ENDLIST"; - InputStream inputStream = - new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream); HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; @@ -82,6 +81,7 @@ public class HlsMediaPlaylistParserTest { assertThat(mediaPlaylist.mediaSequence).isEqualTo(2679); assertThat(mediaPlaylist.version).isEqualTo(3); assertThat(mediaPlaylist.hasEndTag).isTrue(); + assertThat(mediaPlaylist.protectionSchemes).isNull(); List segments = mediaPlaylist.segments; assertThat(segments).isNotNull(); assertThat(segments).hasSize(5); @@ -90,6 +90,7 @@ public class HlsMediaPlaylistParserTest { assertThat(mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence) .isEqualTo(4); assertThat(segment.durationUs).isEqualTo(7975000); + assertThat(segment.title).isEqualTo(""); assertThat(segment.fullSegmentEncryptionKeyUri).isNull(); assertThat(segment.encryptionIV).isNull(); assertThat(segment.byterangeLength).isEqualTo(51370); @@ -99,6 +100,7 @@ public class HlsMediaPlaylistParserTest { segment = segments.get(1); assertThat(segment.relativeDiscontinuitySequence).isEqualTo(0); assertThat(segment.durationUs).isEqualTo(7975000); + assertThat(segment.title).isEqualTo("segment title"); assertThat(segment.fullSegmentEncryptionKeyUri) .isEqualTo("https://priv.example.com/key.php?r=2680"); assertThat(segment.encryptionIV).isEqualTo("0x1566B"); @@ -109,6 +111,7 @@ public class HlsMediaPlaylistParserTest { segment = segments.get(2); assertThat(segment.relativeDiscontinuitySequence).isEqualTo(0); assertThat(segment.durationUs).isEqualTo(7941000); + assertThat(segment.title).isEqualTo("segment title .,:/# with interesting chars"); assertThat(segment.fullSegmentEncryptionKeyUri).isNull(); assertThat(segment.encryptionIV).isEqualTo(null); assertThat(segment.byterangeLength).isEqualTo(51501); @@ -118,6 +121,7 @@ public class HlsMediaPlaylistParserTest { segment = segments.get(3); assertThat(segment.relativeDiscontinuitySequence).isEqualTo(1); assertThat(segment.durationUs).isEqualTo(7975000); + assertThat(segment.title).isEqualTo(""); assertThat(segment.fullSegmentEncryptionKeyUri) .isEqualTo("https://priv.example.com/key.php?r=2682"); // 0xA7A == 2682. @@ -130,6 +134,7 @@ public class HlsMediaPlaylistParserTest { segment = segments.get(4); assertThat(segment.relativeDiscontinuitySequence).isEqualTo(1); assertThat(segment.durationUs).isEqualTo(7975000); + assertThat(segment.title).isEqualTo(""); assertThat(segment.fullSegmentEncryptionKeyUri) .isEqualTo("https://priv.example.com/key.php?r=2682"); // 0xA7B == 2683. @@ -156,12 +161,17 @@ public class HlsMediaPlaylistParserTest { + "#EXTINF:8,\n" + "https://priv.example.com/2.ts\n" + "#EXT-X-ENDLIST\n"; - InputStream inputStream = - new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); HlsMediaPlaylist playlist = (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); - assertThat(playlist.drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cbcs); - assertThat(playlist.drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cbcs); + assertThat(playlist.protectionSchemes.get(0).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse(); + + assertThat(playlist.segments.get(0).drmInitData).isNull(); + + assertThat(playlist.segments.get(1).drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.segments.get(1).drmInitData.get(0).hasData()).isTrue(); } @Test @@ -180,12 +190,12 @@ public class HlsMediaPlaylistParserTest { + "#EXTINF:8,\n" + "https://priv.example.com/2.ts\n" + "#EXT-X-ENDLIST\n"; - InputStream inputStream = - new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); HlsMediaPlaylist playlist = (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); - assertThat(playlist.drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cenc); - assertThat(playlist.drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cenc); + assertThat(playlist.protectionSchemes.get(0).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse(); } @Test @@ -204,12 +214,89 @@ public class HlsMediaPlaylistParserTest { + "#EXTINF:8,\n" + "https://priv.example.com/2.ts\n" + "#EXT-X-ENDLIST\n"; - InputStream inputStream = - new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); HlsMediaPlaylist playlist = (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); - assertThat(playlist.drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cenc); - assertThat(playlist.drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cenc); + assertThat(playlist.protectionSchemes.get(0).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse(); + } + + @Test + public void testMultipleExtXKeysForSingleSegment() throws Exception { + Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-VERSION:6\n" + + "#EXT-X-TARGETDURATION:6\n" + + "#EXT-X-MAP:URI=\"map.mp4\"\n" + + "#EXTINF:5.005,\n" + + "s000000.mp4\n" + + "#EXT-X-KEY:METHOD=SAMPLE-AES," + + "KEYFORMAT=\"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\"," + + "KEYFORMATVERSIONS=\"1\"," + + "URI=\"data:text/plain;base64,Tm90aGluZyB0byBzZWUgaGVyZQ==\"\n" + + "#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\"com.microsoft.playready\"," + + "KEYFORMATVERSIONS=\"1\"," + + "URI=\"data:text/plain;charset=UTF-16;base64,VGhpcyBpcyBhbiBlYXN0ZXIgZWdn\"\n" + + "#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\"com.apple.streamingkeydelivery\"," + + "KEYFORMATVERSIONS=\"1\",URI=\"skd://QW5vdGhlciBlYXN0ZXIgZWdn\"\n" + + "#EXT-X-MAP:URI=\"map.mp4\"\n" + + "#EXTINF:5.005,\n" + + "s000000.mp4\n" + + "#EXTINF:5.005,\n" + + "s000001.mp4\n" + + "#EXT-X-KEY:METHOD=SAMPLE-AES," + + "KEYFORMAT=\"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\"," + + "KEYFORMATVERSIONS=\"1\"," + + "URI=\"data:text/plain;base64,RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==\"" + + "\n" + + "#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\"com.microsoft.playready\"," + + "KEYFORMATVERSIONS=\"1\"," + + "URI=\"data:text/plain;charset=UTF-16;base64,T2ssIGl0J3Mgbm90IGZ1biBhbnltb3Jl\"\n" + + "#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\"com.apple.streamingkeydelivery\"," + + "KEYFORMATVERSIONS=\"1\"," + + "URI=\"skd://V2FpdCB1bnRpbCB5b3Ugc2VlIHRoZSBuZXh0IG9uZSE=\"\n" + + "#EXTINF:5.005,\n" + + "s000024.mp4\n" + + "#EXTINF:5.005,\n" + + "s000025.mp4\n" + + "#EXT-X-KEY:METHOD=NONE\n" + + "#EXTINF:5.005,\n" + + "s000026.mp4\n" + + "#EXTINF:5.005,\n" + + "s000026.mp4\n"; + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); + assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cbcs); + // Unsupported protection schemes like com.apple.streamingkeydelivery are ignored. + assertThat(playlist.protectionSchemes.schemeDataCount).isEqualTo(2); + assertThat(playlist.protectionSchemes.get(0).matches(C.PLAYREADY_UUID)).isTrue(); + assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse(); + assertThat(playlist.protectionSchemes.get(1).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.protectionSchemes.get(1).hasData()).isFalse(); + + assertThat(playlist.segments.get(0).drmInitData).isNull(); + + assertThat(playlist.segments.get(1).drmInitData.get(0).matches(C.PLAYREADY_UUID)).isTrue(); + assertThat(playlist.segments.get(1).drmInitData.get(0).hasData()).isTrue(); + assertThat(playlist.segments.get(1).drmInitData.get(1).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.segments.get(1).drmInitData.get(1).hasData()).isTrue(); + + assertThat(playlist.segments.get(1).drmInitData) + .isEqualTo(playlist.segments.get(2).drmInitData); + assertThat(playlist.segments.get(2).drmInitData) + .isNotEqualTo(playlist.segments.get(3).drmInitData); + assertThat(playlist.segments.get(3).drmInitData.get(0).matches(C.PLAYREADY_UUID)).isTrue(); + assertThat(playlist.segments.get(3).drmInitData.get(0).hasData()).isTrue(); + assertThat(playlist.segments.get(3).drmInitData.get(1).matches(C.WIDEVINE_UUID)).isTrue(); + assertThat(playlist.segments.get(3).drmInitData.get(1).hasData()).isTrue(); + + assertThat(playlist.segments.get(3).drmInitData) + .isEqualTo(playlist.segments.get(4).drmInitData); + assertThat(playlist.segments.get(5).drmInitData).isNull(); + assertThat(playlist.segments.get(6).drmInitData).isNull(); } @Test @@ -237,8 +324,7 @@ public class HlsMediaPlaylistParserTest { + "02/00/42.ts\n" + "#EXTINF:5.005,\n" + "02/00/47.ts\n"; - InputStream inputStream = - new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); HlsMediaPlaylist playlist = (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); @@ -266,8 +352,7 @@ public class HlsMediaPlaylistParserTest { + "#EXT-X-MAP:URI=\"init2.ts\"" + "#EXTINF:5.005,\n" + "02/00/47.ts\n"; - InputStream inputStream = - new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); HlsMediaPlaylist playlist = (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); @@ -278,4 +363,43 @@ public class HlsMediaPlaylistParserTest { assertThat(segments.get(1).initializationSegment.url).isEqualTo("init1.ts"); assertThat(segments.get(3).initializationSegment.url).isEqualTo("init2.ts"); } + + @Test + public void testMasterPlaylistAttributeInheritance() throws IOException { + Uri playlistUri = Uri.parse("https://example.com/test3.m3u8"); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-TARGETDURATION:5\n" + + "#EXT-X-MEDIA-SEQUENCE:10\n" + + "#EXTINF:5.005,\n" + + "02/00/27.ts\n" + + "#EXT-X-MAP:URI=\"init1.ts\"" + + "#EXTINF:5.005,\n" + + "02/00/32.ts\n" + + "#EXTINF:5.005,\n" + + "02/00/42.ts\n" + + "#EXT-X-MAP:URI=\"init2.ts\"" + + "#EXTINF:5.005,\n" + + "02/00/47.ts\n"; + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); + HlsMediaPlaylist standalonePlaylist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); + assertThat(standalonePlaylist.hasIndependentSegments).isFalse(); + + inputStream.reset(); + HlsMasterPlaylist masterPlaylist = + new HlsMasterPlaylist( + /* baseUri= */ "https://example.com/", + /* tags= */ Collections.emptyList(), + /* variants= */ Collections.emptyList(), + /* audios= */ Collections.emptyList(), + /* subtitles= */ Collections.emptyList(), + /* muxedAudioFormat= */ null, + /* muxedCaptionFormats= */ null, + /* hasIndependentSegments= */ true); + HlsMediaPlaylist playlistWithInheritance = + (HlsMediaPlaylist) new HlsPlaylistParser(masterPlaylist).parse(playlistUri, inputStream); + assertThat(playlistWithInheritance.hasIndependentSegments).isTrue(); + } } diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index 6f85d1572d..2fce6b697c 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -18,9 +18,15 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion + consumerProguardFiles 'proguard-rules.txt' } buildTypes { diff --git a/library/smoothstreaming/proguard-rules.txt b/library/smoothstreaming/proguard-rules.txt new file mode 100644 index 0000000000..d14244d783 --- /dev/null +++ b/library/smoothstreaming/proguard-rules.txt @@ -0,0 +1,7 @@ +# Proguard rules specific to the smoothstreaming module. + +# Constructors accessed via reflection in SegmentDownloadAction +-dontnote com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction +-keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction { + static ** DESERIALIZER; +} diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index de236c3514..831d21eeb7 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.smoothstreaming; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; @@ -26,7 +27,6 @@ import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper; import com.google.android.exoplayer2.source.chunk.ChunkHolder; -import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.List; @@ -53,10 +54,17 @@ public class DefaultSsChunkSource implements SsChunkSource { } @Override - public SsChunkSource createChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, - SsManifest manifest, int elementIndex, TrackSelection trackSelection, - TrackEncryptionBox[] trackEncryptionBoxes) { + public SsChunkSource createChunkSource( + LoaderErrorThrower manifestLoaderErrorThrower, + SsManifest manifest, + int elementIndex, + TrackSelection trackSelection, + TrackEncryptionBox[] trackEncryptionBoxes, + @Nullable TransferListener transferListener) { DataSource dataSource = dataSourceFactory.createDataSource(); + if (transferListener != null) { + dataSource.addTransferListener(transferListener); + } return new DefaultSsChunkSource(manifestLoaderErrorThrower, manifest, elementIndex, trackSelection, dataSource, trackEncryptionBoxes); } @@ -166,7 +174,10 @@ public class DefaultSsChunkSource implements SsChunkSource { } @Override - public final void getNextChunk(MediaChunk previous, long playbackPositionUs, long loadPositionUs, + public final void getNextChunk( + long playbackPositionUs, + long loadPositionUs, + List queue, ChunkHolder out) { if (fatalError != null) { return; @@ -180,10 +191,11 @@ public class DefaultSsChunkSource implements SsChunkSource { } int chunkIndex; - if (previous == null) { + if (queue.isEmpty()) { chunkIndex = streamElement.getChunkIndex(loadPositionUs); } else { - chunkIndex = (int) (previous.getNextChunkIndex() - currentManifestChunkOffset); + chunkIndex = + (int) (queue.get(queue.size() - 1).getNextChunkIndex() - currentManifestChunkOffset); if (chunkIndex < 0) { // This is before the first chunk in the current manifest. fatalError = new BehindLiveWindowException(); @@ -203,7 +215,7 @@ public class DefaultSsChunkSource implements SsChunkSource { long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex); long chunkEndTimeUs = chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex); - long chunkSeekTimeUs = previous == null ? loadPositionUs : C.TIME_UNSET; + long chunkSeekTimeUs = queue.isEmpty() ? loadPositionUs : C.TIME_UNSET; int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset; int trackSelectionIndex = trackSelection.getSelectedIndex(); @@ -233,9 +245,11 @@ public class DefaultSsChunkSource implements SsChunkSource { } @Override - public boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e) { - return cancelable && ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection, - trackSelection.indexOf(chunk.trackFormat), e); + public boolean onChunkLoadError( + Chunk chunk, boolean cancelable, Exception e, long blacklistDurationMs) { + return cancelable + && blacklistDurationMs != C.TIME_UNSET + && trackSelection.blacklist(trackSelection.indexOf(chunk.trackFormat), blacklistDurationMs); } // Private methods. diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java index 48491cd0bd..f333a6f92c 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java @@ -15,11 +15,13 @@ */ package com.google.android.exoplayer2.source.smoothstreaming; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer2.source.chunk.ChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; +import com.google.android.exoplayer2.upstream.TransferListener; /** * A {@link ChunkSource} for SmoothStreaming. @@ -37,6 +39,8 @@ public interface SsChunkSource extends ChunkSource { * @param streamElementIndex The index of the corresponding stream element in the manifest. * @param trackSelection The track selection. * @param trackEncryptionBoxes Track encryption boxes for the stream. + * @param transferListener The transfer listener which should be informed of any data transfers. + * May be null if no listener is available. * @return The created {@link SsChunkSource}. */ SsChunkSource createChunkSource( @@ -44,7 +48,8 @@ public interface SsChunkSource extends ChunkSource { SsManifest manifest, int streamElementIndex, TrackSelection trackSelection, - TrackEncryptionBox[] trackEncryptionBoxes); + TrackEncryptionBox[] trackEncryptionBoxes, + @Nullable TransferListener transferListener); } /** diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 9a0d57ff31..14b54bc471 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.smoothstreaming; +import android.support.annotation.Nullable; import android.util.Base64; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SeekParameters; @@ -31,7 +32,9 @@ import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; +import com.google.android.exoplayer2.upstream.TransferListener; import java.io.IOException; import java.util.ArrayList; @@ -44,27 +47,34 @@ import java.util.ArrayList; private static final int INITIALIZATION_VECTOR_SIZE = 8; private final SsChunkSource.Factory chunkSourceFactory; + private final @Nullable TransferListener transferListener; private final LoaderErrorThrower manifestLoaderErrorThrower; - private final int minLoadableRetryCount; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Allocator allocator; private final TrackGroupArray trackGroups; private final TrackEncryptionBox[] trackEncryptionBoxes; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private Callback callback; + private @Nullable Callback callback; private SsManifest manifest; private ChunkSampleStream[] sampleStreams; private SequenceableLoader compositeSequenceableLoader; private boolean notifiedReadingStarted; - public SsMediaPeriod(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, + public SsMediaPeriod( + SsManifest manifest, + SsChunkSource.Factory chunkSourceFactory, + @Nullable TransferListener transferListener, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, - int minLoadableRetryCount, EventDispatcher eventDispatcher, - LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator) { + LoadErrorHandlingPolicy loadErrorHandlingPolicy, + EventDispatcher eventDispatcher, + LoaderErrorThrower manifestLoaderErrorThrower, + Allocator allocator) { this.chunkSourceFactory = chunkSourceFactory; + this.transferListener = transferListener; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; - this.minLoadableRetryCount = minLoadableRetryCount; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.allocator = allocator; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; @@ -98,6 +108,7 @@ import java.util.ArrayList; for (ChunkSampleStream sampleStream : sampleStreams) { sampleStream.release(); } + callback = null; eventDispatcher.mediaPeriodReleased(); } @@ -212,8 +223,14 @@ import java.util.ArrayList; private ChunkSampleStream buildSampleStream(TrackSelection selection, long positionUs) { int streamElementIndex = trackGroups.indexOf(selection.getTrackGroup()); - SsChunkSource chunkSource = chunkSourceFactory.createChunkSource(manifestLoaderErrorThrower, - manifest, streamElementIndex, selection, trackEncryptionBoxes); + SsChunkSource chunkSource = + chunkSourceFactory.createChunkSource( + manifestLoaderErrorThrower, + manifest, + streamElementIndex, + selection, + trackEncryptionBoxes, + transferListener); return new ChunkSampleStream<>( manifest.streamElements[streamElementIndex].type, null, @@ -222,7 +239,7 @@ import java.util.ArrayList; this, allocator, positionUs, - minLoadableRetryCount, + loadErrorHandlingPolicy, eventDispatcher); } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 72d1ba1efd..1a80ade01d 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -40,9 +40,13 @@ import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestP import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsUtil; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.Loader; +import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; @@ -63,11 +67,21 @@ public final class SsMediaSource extends BaseMediaSource private @Nullable ParsingLoadable.Parser manifestParser; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private int minLoadableRetryCount; + private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private long livePresentationDelayMs; private boolean isCreateCalled; private @Nullable Object tag; + /** + * Creates a new factory for {@link SsMediaSource}s. + * + * @param dataSourceFactory A factory for {@link DataSource} instances that will be used to load + * manifest and media data. + */ + public Factory(DataSource.Factory dataSourceFactory) { + this(new DefaultSsChunkSource.Factory(dataSourceFactory), dataSourceFactory); + } + /** * Creates a new factory for {@link SsMediaSource}s. * @@ -82,7 +96,7 @@ public final class SsMediaSource extends BaseMediaSource @Nullable DataSource.Factory manifestDataSourceFactory) { this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; - minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; + loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } @@ -102,16 +116,36 @@ public final class SsMediaSource extends BaseMediaSource } /** - * Sets the minimum number of times to retry if a loading error occurs. The default value is - * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * Sets the minimum number of times to retry if a loading error occurs. See {@link + * #setLoadErrorHandlingPolicy} for the default value. + * + *

    Calling this method is equivalent to calling {@link #setLoadErrorHandlingPolicy} with + * {@link DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy(int) + * DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)} * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead. */ + @Deprecated public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)); + } + + /** + * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link + * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. + * + *

    Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}. + * + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { Assertions.checkState(!isCreateCalled); - this.minLoadableRetryCount = minLoadableRetryCount; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; return this; } @@ -181,7 +215,7 @@ public final class SsMediaSource extends BaseMediaSource /* manifestParser= */ null, chunkSourceFactory, compositeSequenceableLoaderFactory, - minLoadableRetryCount, + loadErrorHandlingPolicy, livePresentationDelayMs, tag); } @@ -221,7 +255,7 @@ public final class SsMediaSource extends BaseMediaSource manifestParser, chunkSourceFactory, compositeSequenceableLoaderFactory, - minLoadableRetryCount, + loadErrorHandlingPolicy, livePresentationDelayMs, tag); } @@ -249,10 +283,6 @@ public final class SsMediaSource extends BaseMediaSource } - /** - * The default minimum number of times to retry loading data prior to failing. - */ - public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; /** * The default presentation delay for live streams. The presentation delay is the duration by * which the default start position precedes the end of the live window. @@ -273,7 +303,7 @@ public final class SsMediaSource extends BaseMediaSource private final DataSource.Factory manifestDataSourceFactory; private final SsChunkSource.Factory chunkSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private final int minLoadableRetryCount; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long livePresentationDelayMs; private final EventDispatcher manifestEventDispatcher; private final ParsingLoadable.Parser manifestParser; @@ -283,6 +313,7 @@ public final class SsMediaSource extends BaseMediaSource private DataSource manifestDataSource; private Loader manifestLoader; private LoaderErrorThrower manifestLoaderErrorThrower; + private @Nullable TransferListener mediaTransferListener; private long manifestLoadStartTimestamp; private SsManifest manifest; @@ -304,8 +335,12 @@ public final class SsMediaSource extends BaseMediaSource SsChunkSource.Factory chunkSourceFactory, Handler eventHandler, MediaSourceEventListener eventListener) { - this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, - eventHandler, eventListener); + this( + manifest, + chunkSourceFactory, + DefaultLoadErrorHandlingPolicy.DEFAULT_MIN_LOADABLE_RETRY_COUNT, + eventHandler, + eventListener); } /** @@ -332,7 +367,7 @@ public final class SsMediaSource extends BaseMediaSource /* manifestParser= */ null, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), - minLoadableRetryCount, + new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), DEFAULT_LIVE_PRESENTATION_DELAY_MS, /* tag= */ null); if (eventHandler != null && eventListener != null) { @@ -359,8 +394,13 @@ public final class SsMediaSource extends BaseMediaSource SsChunkSource.Factory chunkSourceFactory, Handler eventHandler, MediaSourceEventListener eventListener) { - this(manifestUri, manifestDataSourceFactory, chunkSourceFactory, - DEFAULT_MIN_LOADABLE_RETRY_COUNT, DEFAULT_LIVE_PRESENTATION_DELAY_MS, eventHandler, + this( + manifestUri, + manifestDataSourceFactory, + chunkSourceFactory, + DefaultLoadErrorHandlingPolicy.DEFAULT_MIN_LOADABLE_RETRY_COUNT, + DEFAULT_LIVE_PRESENTATION_DELAY_MS, + eventHandler, eventListener); } @@ -425,7 +465,7 @@ public final class SsMediaSource extends BaseMediaSource manifestParser, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), - minLoadableRetryCount, + new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), livePresentationDelayMs, /* tag= */ null); if (eventHandler != null && eventListener != null) { @@ -440,7 +480,7 @@ public final class SsMediaSource extends BaseMediaSource ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, - int minLoadableRetryCount, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, long livePresentationDelayMs, @Nullable Object tag) { Assertions.checkState(manifest == null || !manifest.isLive); @@ -450,7 +490,7 @@ public final class SsMediaSource extends BaseMediaSource this.manifestParser = manifestParser; this.chunkSourceFactory = chunkSourceFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; - this.minLoadableRetryCount = minLoadableRetryCount; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.livePresentationDelayMs = livePresentationDelayMs; this.manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); this.tag = tag; @@ -461,7 +501,11 @@ public final class SsMediaSource extends BaseMediaSource // MediaSource implementation. @Override - public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + public void prepareSourceInternal( + ExoPlayer player, + boolean isTopLevelSource, + @Nullable TransferListener mediaTransferListener) { + this.mediaTransferListener = mediaTransferListener; if (sideloadedManifest) { manifestLoaderErrorThrower = new LoaderErrorThrower.Dummy(); processManifest(); @@ -483,9 +527,16 @@ public final class SsMediaSource extends BaseMediaSource public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); EventDispatcher eventDispatcher = createEventDispatcher(id); - SsMediaPeriod period = new SsMediaPeriod(manifest, chunkSourceFactory, - compositeSequenceableLoaderFactory, minLoadableRetryCount, eventDispatcher, - manifestLoaderErrorThrower, allocator); + SsMediaPeriod period = + new SsMediaPeriod( + manifest, + chunkSourceFactory, + mediaTransferListener, + compositeSequenceableLoaderFactory, + loadErrorHandlingPolicy, + eventDispatcher, + manifestLoaderErrorThrower, + allocator); mediaPeriods.add(period); return period; } @@ -518,6 +569,7 @@ public final class SsMediaSource extends BaseMediaSource long loadDurationMs) { manifestEventDispatcher.loadCompleted( loadable.dataSpec, + loadable.getUri(), loadable.type, elapsedRealtimeMs, loadDurationMs, @@ -533,6 +585,7 @@ public final class SsMediaSource extends BaseMediaSource long loadDurationMs, boolean released) { manifestEventDispatcher.loadCanceled( loadable.dataSpec, + loadable.getUri(), loadable.type, elapsedRealtimeMs, loadDurationMs, @@ -540,14 +593,16 @@ public final class SsMediaSource extends BaseMediaSource } @Override - public @Loader.RetryAction int onLoadError( + public LoadErrorAction onLoadError( ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, - IOException error) { + IOException error, + int errorCount) { boolean isFatal = error instanceof ParserException; manifestEventDispatcher.loadError( loadable.dataSpec, + loadable.getUri(), loadable.type, elapsedRealtimeMs, loadDurationMs, @@ -640,8 +695,11 @@ public final class SsMediaSource extends BaseMediaSource private void startLoadingManifest() { ParsingLoadable loadable = new ParsingLoadable<>(manifestDataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser); - long elapsedRealtimeMs = manifestLoader.startLoading(loadable, this, minLoadableRetryCount); - manifestEventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); + long elapsedRealtimeMs = + manifestLoader.startLoading( + loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)); + manifestEventDispatcher.loadStarted( + loadable.dataSpec, loadable.dataSpec.uri, loadable.type, elapsedRealtimeMs); } } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java index 396d29fb75..51284f06c4 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java @@ -19,6 +19,10 @@ import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.offline.FilterableManifest; +import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator; +import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; +import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.Util; @@ -33,125 +37,9 @@ import java.util.UUID; * @see IIS Smooth * Streaming Client Manifest Format */ -public class SsManifest implements FilterableManifest { +public class SsManifest implements FilterableManifest { - public static final int UNSET_LOOKAHEAD = -1; - - /** - * The client manifest major version. - */ - public final int majorVersion; - - /** - * The client manifest minor version. - */ - public final int minorVersion; - - /** - * The number of fragments in a lookahead, or {@link #UNSET_LOOKAHEAD} if the lookahead is - * unspecified. - */ - public final int lookAheadCount; - - /** - * Whether the manifest describes a live presentation still in progress. - */ - public final boolean isLive; - - /** - * Content protection information, or null if the content is not protected. - */ - public final ProtectionElement protectionElement; - - /** - * The contained stream elements. - */ - public final StreamElement[] streamElements; - - /** - * The overall presentation duration of the media in microseconds, or {@link C#TIME_UNSET} - * if the duration is unknown. - */ - public final long durationUs; - - /** - * The length of the trailing window for a live broadcast in microseconds, or - * {@link C#TIME_UNSET} if the stream is not live or if the window length is unspecified. - */ - public final long dvrWindowLengthUs; - - /** - * @param majorVersion The client manifest major version. - * @param minorVersion The client manifest minor version. - * @param timescale The timescale of the media as the number of units that pass in one second. - * @param duration The overall presentation duration in units of the timescale attribute, or 0 - * if the duration is unknown. - * @param dvrWindowLength The length of the trailing window in units of the timescale attribute, - * or 0 if this attribute is unspecified or not applicable. - * @param lookAheadCount The number of fragments in a lookahead, or {@link #UNSET_LOOKAHEAD} if - * this attribute is unspecified or not applicable. - * @param isLive True if the manifest describes a live presentation still in progress. False - * otherwise. - * @param protectionElement Content protection information, or null if the content is not - * protected. - * @param streamElements The contained stream elements. - */ - public SsManifest(int majorVersion, int minorVersion, long timescale, long duration, - long dvrWindowLength, int lookAheadCount, boolean isLive, ProtectionElement protectionElement, - StreamElement[] streamElements) { - this(majorVersion, minorVersion, - duration == 0 ? C.TIME_UNSET - : Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, timescale), - dvrWindowLength == 0 ? C.TIME_UNSET - : Util.scaleLargeTimestamp(dvrWindowLength, C.MICROS_PER_SECOND, timescale), - lookAheadCount, isLive, protectionElement, streamElements); - } - - private SsManifest(int majorVersion, int minorVersion, long durationUs, long dvrWindowLengthUs, - int lookAheadCount, boolean isLive, ProtectionElement protectionElement, - StreamElement[] streamElements) { - this.majorVersion = majorVersion; - this.minorVersion = minorVersion; - this.durationUs = durationUs; - this.dvrWindowLengthUs = dvrWindowLengthUs; - this.lookAheadCount = lookAheadCount; - this.isLive = isLive; - this.protectionElement = protectionElement; - this.streamElements = streamElements; - } - - @Override - public final SsManifest copy(List streamKeys) { - ArrayList sortedKeys = new ArrayList<>(streamKeys); - Collections.sort(sortedKeys); - - StreamElement currentStreamElement = null; - List copiedStreamElements = new ArrayList<>(); - List copiedFormats = new ArrayList<>(); - for (int i = 0; i < sortedKeys.size(); i++) { - StreamKey key = sortedKeys.get(i); - StreamElement streamElement = streamElements[key.streamElementIndex]; - if (streamElement != currentStreamElement && currentStreamElement != null) { - // We're advancing to a new stream element. Add the current one. - copiedStreamElements.add(currentStreamElement.copy(copiedFormats.toArray(new Format[0]))); - copiedFormats.clear(); - } - currentStreamElement = streamElement; - copiedFormats.add(streamElement.formats[key.trackIndex]); - } - if (currentStreamElement != null) { - // Add the last stream element. - copiedStreamElements.add(currentStreamElement.copy(copiedFormats.toArray(new Format[0]))); - } - - StreamElement[] copiedStreamElementsArray = copiedStreamElements.toArray(new StreamElement[0]); - return new SsManifest(majorVersion, minorVersion, durationUs, dvrWindowLengthUs, lookAheadCount, - isLive, protectionElement, copiedStreamElementsArray); - } - - /** - * Represents a protection element containing a single header. - */ + /** Represents a protection element containing a single header. */ public static class ProtectionElement { public final UUID uuid; @@ -161,7 +49,45 @@ public class SsManifest implements FilterableManifest { this.uuid = uuid; this.data = data; } + } + /** {@link MediaChunkIterator} wrapping a track of a {@link StreamElement}. */ + public static final class StreamElementIterator extends BaseMediaChunkIterator { + + private final StreamElement streamElement; + private final int trackIndex; + + /** + * Creates iterator. + * + * @param streamElement The {@link StreamElement} to wrap. + * @param trackIndex The track index in the stream element. + * @param chunkIndex The chunk index at which the iterator will start. + */ + public StreamElementIterator(StreamElement streamElement, int trackIndex, int chunkIndex) { + super(/* fromIndex= */ chunkIndex, /* toIndex= */ streamElement.chunkCount - 1); + this.streamElement = streamElement; + this.trackIndex = trackIndex; + } + + @Override + public DataSpec getDataSpec() { + checkInBounds(); + Uri uri = streamElement.buildRequestUri(trackIndex, (int) getCurrentIndex()); + return new DataSpec(uri); + } + + @Override + public long getChunkStartTimeUs() { + checkInBounds(); + return streamElement.getStartTimeUs((int) getCurrentIndex()); + } + + @Override + public long getChunkEndTimeUs() { + long chunkStartTimeUs = getChunkStartTimeUs(); + return chunkStartTimeUs + streamElement.getChunkDurationUs((int) getCurrentIndex()); + } } /** @@ -301,7 +227,136 @@ public class SsManifest implements FilterableManifest { .replace(URL_PLACEHOLDER_START_TIME_2, startTimeString); return UriUtil.resolveToUri(baseUri, chunkUrl); } - } + public static final int UNSET_LOOKAHEAD = -1; + + /** The client manifest major version. */ + public final int majorVersion; + + /** The client manifest minor version. */ + public final int minorVersion; + + /** + * The number of fragments in a lookahead, or {@link #UNSET_LOOKAHEAD} if the lookahead is + * unspecified. + */ + public final int lookAheadCount; + + /** Whether the manifest describes a live presentation still in progress. */ + public final boolean isLive; + + /** Content protection information, or null if the content is not protected. */ + public final ProtectionElement protectionElement; + + /** The contained stream elements. */ + public final StreamElement[] streamElements; + + /** + * The overall presentation duration of the media in microseconds, or {@link C#TIME_UNSET} if the + * duration is unknown. + */ + public final long durationUs; + + /** + * The length of the trailing window for a live broadcast in microseconds, or {@link C#TIME_UNSET} + * if the stream is not live or if the window length is unspecified. + */ + public final long dvrWindowLengthUs; + + /** + * @param majorVersion The client manifest major version. + * @param minorVersion The client manifest minor version. + * @param timescale The timescale of the media as the number of units that pass in one second. + * @param duration The overall presentation duration in units of the timescale attribute, or 0 if + * the duration is unknown. + * @param dvrWindowLength The length of the trailing window in units of the timescale attribute, + * or 0 if this attribute is unspecified or not applicable. + * @param lookAheadCount The number of fragments in a lookahead, or {@link #UNSET_LOOKAHEAD} if + * this attribute is unspecified or not applicable. + * @param isLive True if the manifest describes a live presentation still in progress. False + * otherwise. + * @param protectionElement Content protection information, or null if the content is not + * protected. + * @param streamElements The contained stream elements. + */ + public SsManifest( + int majorVersion, + int minorVersion, + long timescale, + long duration, + long dvrWindowLength, + int lookAheadCount, + boolean isLive, + ProtectionElement protectionElement, + StreamElement[] streamElements) { + this( + majorVersion, + minorVersion, + duration == 0 + ? C.TIME_UNSET + : Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, timescale), + dvrWindowLength == 0 + ? C.TIME_UNSET + : Util.scaleLargeTimestamp(dvrWindowLength, C.MICROS_PER_SECOND, timescale), + lookAheadCount, + isLive, + protectionElement, + streamElements); + } + + private SsManifest( + int majorVersion, + int minorVersion, + long durationUs, + long dvrWindowLengthUs, + int lookAheadCount, + boolean isLive, + ProtectionElement protectionElement, + StreamElement[] streamElements) { + this.majorVersion = majorVersion; + this.minorVersion = minorVersion; + this.durationUs = durationUs; + this.dvrWindowLengthUs = dvrWindowLengthUs; + this.lookAheadCount = lookAheadCount; + this.isLive = isLive; + this.protectionElement = protectionElement; + this.streamElements = streamElements; + } + + @Override + public final SsManifest copy(List streamKeys) { + ArrayList sortedKeys = new ArrayList<>(streamKeys); + Collections.sort(sortedKeys); + + StreamElement currentStreamElement = null; + List copiedStreamElements = new ArrayList<>(); + List copiedFormats = new ArrayList<>(); + for (int i = 0; i < sortedKeys.size(); i++) { + StreamKey key = sortedKeys.get(i); + StreamElement streamElement = streamElements[key.groupIndex]; + if (streamElement != currentStreamElement && currentStreamElement != null) { + // We're advancing to a new stream element. Add the current one. + copiedStreamElements.add(currentStreamElement.copy(copiedFormats.toArray(new Format[0]))); + copiedFormats.clear(); + } + currentStreamElement = streamElement; + copiedFormats.add(streamElement.formats[key.trackIndex]); + } + if (currentStreamElement != null) { + // Add the last stream element. + copiedStreamElements.add(currentStreamElement.copy(copiedFormats.toArray(new Format[0]))); + } + + StreamElement[] copiedStreamElementsArray = copiedStreamElements.toArray(new StreamElement[0]); + return new SsManifest( + majorVersion, + minorVersion, + durationUs, + dvrWindowLengthUs, + lookAheadCount, + isLive, + protectionElement, + copiedStreamElementsArray); + } } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java index 3ca5f8d997..c2437db189 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java @@ -603,6 +603,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { private static final String KEY_FOUR_CC = "FourCC"; private static final String KEY_TYPE = "Type"; private static final String KEY_LANGUAGE = "Language"; + private static final String KEY_NAME = "Name"; private static final String KEY_MAX_WIDTH = "MaxWidth"; private static final String KEY_MAX_HEIGHT = "MaxHeight"; @@ -616,6 +617,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { public void parseStartTag(XmlPullParser parser) throws ParserException { int type = (Integer) getNormalizedAttribute(KEY_TYPE); String id = parser.getAttributeValue(null, KEY_INDEX); + String name = (String) getNormalizedAttribute(KEY_NAME); int bitrate = parseRequiredInt(parser, KEY_BITRATE); String sampleMimeType = fourCCToMimeType(parseRequiredString(parser, KEY_FOUR_CC)); @@ -624,8 +626,19 @@ public class SsManifestParser implements ParsingLoadable.Parser { int height = parseRequiredInt(parser, KEY_MAX_HEIGHT); List codecSpecificData = buildCodecSpecificData( parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA)); - format = Format.createVideoContainerFormat(id, MimeTypes.VIDEO_MP4, sampleMimeType, null, - bitrate, width, height, Format.NO_VALUE, codecSpecificData, 0); + format = + Format.createVideoContainerFormat( + id, + name, + MimeTypes.VIDEO_MP4, + sampleMimeType, + /* codecs= */ null, + bitrate, + width, + height, + /* frameRate= */ Format.NO_VALUE, + codecSpecificData, + /* selectionFlags= */ 0); } else if (type == C.TRACK_TYPE_AUDIO) { sampleMimeType = sampleMimeType == null ? MimeTypes.AUDIO_AAC : sampleMimeType; int channels = parseRequiredInt(parser, KEY_CHANNELS); @@ -637,15 +650,42 @@ public class SsManifestParser implements ParsingLoadable.Parser { CodecSpecificDataUtil.buildAacLcAudioSpecificConfig(samplingRate, channels)); } String language = (String) getNormalizedAttribute(KEY_LANGUAGE); - format = Format.createAudioContainerFormat(id, MimeTypes.AUDIO_MP4, sampleMimeType, null, - bitrate, channels, samplingRate, codecSpecificData, 0, language); + format = + Format.createAudioContainerFormat( + id, + name, + MimeTypes.AUDIO_MP4, + sampleMimeType, + /* codecs= */ null, + bitrate, + channels, + samplingRate, + codecSpecificData, + /* selectionFlags= */ 0, + language); } else if (type == C.TRACK_TYPE_TEXT) { String language = (String) getNormalizedAttribute(KEY_LANGUAGE); - format = Format.createTextContainerFormat(id, MimeTypes.APPLICATION_MP4, sampleMimeType, - null, bitrate, 0, language); + format = + Format.createTextContainerFormat( + id, + name, + MimeTypes.APPLICATION_MP4, + sampleMimeType, + /* codecs= */ null, + bitrate, + /* selectionFlags= */ 0, + language); } else { - format = Format.createContainerFormat(id, MimeTypes.APPLICATION_MP4, sampleMimeType, null, - bitrate, 0, null); + format = + Format.createContainerFormat( + id, + name, + MimeTypes.APPLICATION_MP4, + sampleMimeType, + /* codecs= */ null, + bitrate, + /* selectionFlags= */ 0, + /* language= */ null); } } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java index d4b3ef6622..ad2196fd74 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java @@ -20,24 +20,29 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.SegmentDownloadAction; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.StreamKey; +import com.google.android.exoplayer2.offline.StreamKey; import java.io.DataInputStream; -import java.io.DataOutputStream; import java.io.IOException; +import java.util.Collections; import java.util.List; /** An action to download or remove downloaded SmoothStreaming streams. */ -public final class SsDownloadAction extends SegmentDownloadAction { +public final class SsDownloadAction extends SegmentDownloadAction { private static final String TYPE = "ss"; - private static final int VERSION = 0; + private static final int VERSION = 1; public static final Deserializer DESERIALIZER = - new SegmentDownloadActionDeserializer(TYPE, VERSION) { + new SegmentDownloadActionDeserializer(TYPE, VERSION) { @Override - protected StreamKey readKey(DataInputStream input) throws IOException { - return new StreamKey(input.readInt(), input.readInt()); + protected StreamKey readKey(int version, DataInputStream input) throws IOException { + if (version > 0) { + return super.readKey(version, input); + } + int groupIndex = input.readInt(); + int trackIndex = input.readInt(); + return new StreamKey(groupIndex, trackIndex); } @Override @@ -47,27 +52,46 @@ public final class SsDownloadAction extends SegmentDownloadAction { } }; + /** + * Creates a SmoothStreaming download action. + * + * @param uri The URI of the media to be downloaded. + * @param data Optional custom data for this action. If {@code null} an empty array will be used. + * @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded. + */ + public static SsDownloadAction createDownloadAction( + Uri uri, @Nullable byte[] data, List keys) { + return new SsDownloadAction(uri, /* isRemoveAction= */ false, data, keys); + } + + /** + * Creates a SmoothStreaming remove action. + * + * @param uri The URI of the media to be removed. + * @param data Optional custom data for this action. If {@code null} an empty array will be used. + */ + public static SsDownloadAction createRemoveAction(Uri uri, @Nullable byte[] data) { + return new SsDownloadAction(uri, /* isRemoveAction= */ true, data, Collections.emptyList()); + } + /** * @param uri The SmoothStreaming manifest URI. * @param isRemoveAction Whether the data will be removed. If {@code false} it will be downloaded. * @param data Optional custom data for this action. * @param keys Keys of streams to be downloaded. If empty, all streams are downloaded. If {@code * removeAction} is true, {@code keys} must be empty. + * @deprecated Use {@link #createDownloadAction(Uri, byte[], List)} or {@link + * #createRemoveAction(Uri, byte[])}. */ + @Deprecated public SsDownloadAction( Uri uri, boolean isRemoveAction, @Nullable byte[] data, List keys) { super(TYPE, VERSION, uri, isRemoveAction, data, keys); } @Override - protected SsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { + public SsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { return new SsDownloader(uri, keys, constructorHelper); } - @Override - protected void writeKey(DataOutputStream output, StreamKey key) throws IOException { - output.writeInt(key.streamElementIndex); - output.writeInt(key.trackIndex); - } - } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java index e60be93c93..5125beff1c 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java @@ -17,19 +17,19 @@ package com.google.android.exoplayer2.source.smoothstreaming.offline; import android.net.Uri; import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.TrackKey; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.StreamKey; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -49,7 +49,7 @@ public final class SsDownloadHelper extends DownloadHelper { @Override protected void prepareInternal() throws IOException { DataSource dataSource = manifestDataSourceFactory.createDataSource(); - manifest = ParsingLoadable.load(dataSource, new SsManifestParser(), uri); + manifest = ParsingLoadable.load(dataSource, new SsManifestParser(), uri, C.DATA_TYPE_MANIFEST); } /** Returns the SmoothStreaming manifest. Must not be called until after preparation completes. */ @@ -77,13 +77,12 @@ public final class SsDownloadHelper extends DownloadHelper { @Override public SsDownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { - return new SsDownloadAction(uri, /* isRemoveAction= */ false, data, toStreamKeys(trackKeys)); + return SsDownloadAction.createDownloadAction(uri, data, toStreamKeys(trackKeys)); } @Override public SsDownloadAction getRemoveAction(@Nullable byte[] data) { - return new SsDownloadAction( - uri, /* isRemoveAction= */ true, data, Collections.emptyList()); + return SsDownloadAction.createRemoveAction(uri, data); } private static List toStreamKeys(List trackKeys) { diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java index 4fef3eb469..84ef251e5f 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java @@ -19,11 +19,11 @@ import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.SegmentDownloader; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsUtil; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.StreamKey; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.ParsingLoadable; @@ -54,7 +54,7 @@ import java.util.List; * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE); * } */ -public final class SsDownloader extends SegmentDownloader { +public final class SsDownloader extends SegmentDownloader { /** * @param manifestUri The {@link Uri} of the manifest to be downloaded. @@ -69,10 +69,7 @@ public final class SsDownloader extends SegmentDownloader @Override protected SsManifest getManifest(DataSource dataSource, Uri uri) throws IOException { - ParsingLoadable loadable = - new ParsingLoadable<>(dataSource, uri, C.DATA_TYPE_MANIFEST, new SsManifestParser()); - loadable.load(); - return loadable.getResult(); + return ParsingLoadable.load(dataSource, new SsManifestParser(), uri, C.DATA_TYPE_MANIFEST); } @Override diff --git a/library/smoothstreaming/src/main/proguard-rules.txt b/library/smoothstreaming/src/main/proguard-rules.txt new file mode 100644 index 0000000000..d14244d783 --- /dev/null +++ b/library/smoothstreaming/src/main/proguard-rules.txt @@ -0,0 +1,7 @@ +# Proguard rules specific to the smoothstreaming module. + +# Constructors accessed via reflection in SegmentDownloadAction +-dontnote com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction +-keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction { + static ** DESERIALIZER; +} diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java index c7c6c6f3fb..05f2582f0d 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; import com.google.android.exoplayer2.util.MimeTypes; @@ -128,12 +129,19 @@ public class SsManifestTest { 768, null, formats, - Collections.emptyList(), + Collections.emptyList(), 0); } private static Format newFormat(String id) { return Format.createContainerFormat( - id, MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, Format.NO_VALUE, 0, null); + id, + /* label= */ null, + MimeTypes.VIDEO_MP4, + MimeTypes.VIDEO_H264, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* selectionFlags= */ 0, + /* language= */ null); } } diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadActionTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadActionTest.java new file mode 100644 index 0000000000..fea03902ec --- /dev/null +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadActionTest.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.smoothstreaming.offline; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +/** Unit tests for {@link SsDownloadAction}. */ +@RunWith(RobolectricTestRunner.class) +public class SsDownloadActionTest { + + private Uri uri1; + private Uri uri2; + + @Before + public void setUp() { + uri1 = Uri.parse("http://test/1.uri"); + uri2 = Uri.parse("http://test/2.uri"); + } + + @Test + public void testDownloadActionIsNotRemoveAction() { + DownloadAction action = createDownloadAction(uri1); + assertThat(action.isRemoveAction).isFalse(); + } + + @Test + public void testRemoveActionIsRemoveAction() { + DownloadAction action2 = createRemoveAction(uri1); + assertThat(action2.isRemoveAction).isTrue(); + } + + @Test + public void testCreateDownloader() { + MockitoAnnotations.initMocks(this); + DownloadAction action = createDownloadAction(uri1); + DownloaderConstructorHelper constructorHelper = + new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); + assertThat(action.createDownloader(constructorHelper)).isNotNull(); + } + + @Test + public void testSameUriDifferentAction_IsSameMedia() { + DownloadAction action1 = createRemoveAction(uri1); + DownloadAction action2 = createDownloadAction(uri1); + assertThat(action1.isSameMedia(action2)).isTrue(); + } + + @Test + public void testDifferentUriAndAction_IsNotSameMedia() { + DownloadAction action3 = createRemoveAction(uri2); + DownloadAction action4 = createDownloadAction(uri1); + assertThat(action3.isSameMedia(action4)).isFalse(); + } + + @SuppressWarnings("EqualsWithItself") + @Test + public void testEquals() { + DownloadAction action1 = createRemoveAction(uri1); + assertThat(action1.equals(action1)).isTrue(); + + DownloadAction action2 = createRemoveAction(uri1); + DownloadAction action3 = createRemoveAction(uri1); + assertEqual(action2, action3); + + DownloadAction action4 = createRemoveAction(uri1); + DownloadAction action5 = createDownloadAction(uri1); + assertNotEqual(action4, action5); + + DownloadAction action6 = createDownloadAction(uri1); + DownloadAction action7 = createDownloadAction(uri1, new StreamKey(0, 0)); + assertNotEqual(action6, action7); + + DownloadAction action8 = createDownloadAction(uri1, new StreamKey(1, 1)); + DownloadAction action9 = createDownloadAction(uri1, new StreamKey(0, 0)); + assertNotEqual(action8, action9); + + DownloadAction action10 = createRemoveAction(uri1); + DownloadAction action11 = createRemoveAction(uri2); + assertNotEqual(action10, action11); + + DownloadAction action12 = createDownloadAction(uri1, new StreamKey(0, 0), new StreamKey(1, 1)); + DownloadAction action13 = createDownloadAction(uri1, new StreamKey(1, 1), new StreamKey(0, 0)); + assertEqual(action12, action13); + + DownloadAction action14 = createDownloadAction(uri1, new StreamKey(0, 0)); + DownloadAction action15 = createDownloadAction(uri1, new StreamKey(1, 1), new StreamKey(0, 0)); + assertNotEqual(action14, action15); + + DownloadAction action16 = createDownloadAction(uri1); + DownloadAction action17 = createDownloadAction(uri1); + assertEqual(action16, action17); + } + + @Test + public void testSerializerGetType() { + DownloadAction action = createDownloadAction(uri1); + assertThat(action.type).isNotNull(); + } + + @Test + public void testSerializerWriteRead() throws Exception { + doTestSerializationRoundTrip(createDownloadAction(uri1)); + doTestSerializationRoundTrip(createRemoveAction(uri1)); + doTestSerializationRoundTrip( + createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1))); + } + + @Test + public void testSerializerVersion0() throws Exception { + doTestSerializationV0RoundTrip(createDownloadAction(uri1)); + doTestSerializationV0RoundTrip(createRemoveAction(uri1)); + doTestSerializationV0RoundTrip( + createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1))); + } + + private static void assertNotEqual(DownloadAction action1, DownloadAction action2) { + assertThat(action1).isNotEqualTo(action2); + assertThat(action2).isNotEqualTo(action1); + } + + private static void assertEqual(DownloadAction action1, DownloadAction action2) { + assertThat(action1).isEqualTo(action2); + assertThat(action2).isEqualTo(action1); + } + + private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(out); + DownloadAction.serializeToStream(action, output); + + assertEqual(action, deserializeActionFromStream(out)); + } + + private static void doTestSerializationV0RoundTrip(SsDownloadAction action) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(out); + DataOutputStream dataOutputStream = new DataOutputStream(output); + dataOutputStream.writeUTF(action.type); + dataOutputStream.writeInt(/* version */ 0); + dataOutputStream.writeUTF(action.uri.toString()); + dataOutputStream.writeBoolean(action.isRemoveAction); + dataOutputStream.writeInt(action.data.length); + dataOutputStream.write(action.data); + dataOutputStream.writeInt(action.keys.size()); + for (int i = 0; i < action.keys.size(); i++) { + StreamKey key = action.keys.get(i); + dataOutputStream.writeInt(key.groupIndex); + dataOutputStream.writeInt(key.trackIndex); + } + dataOutputStream.flush(); + + assertEqual(action, deserializeActionFromStream(out)); + } + + private static DownloadAction deserializeActionFromStream(ByteArrayOutputStream out) + throws IOException { + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + DataInputStream input = new DataInputStream(in); + return DownloadAction.deserializeFromStream( + new DownloadAction.Deserializer[] {SsDownloadAction.DESERIALIZER}, input); + } + + private static SsDownloadAction createDownloadAction(Uri uri, StreamKey... keys) { + ArrayList keysList = new ArrayList<>(); + Collections.addAll(keysList, keys); + return SsDownloadAction.createDownloadAction(uri, null, keysList); + } + + private static SsDownloadAction createRemoveAction(Uri uri) { + return SsDownloadAction.createRemoveAction(uri, null); + } +} diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 42ec0bba0a..367f15f028 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion @@ -37,6 +42,7 @@ dependencies { implementation 'com.android.support:support-media-compat:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + testImplementation project(modulePrefix + 'testutils-robolectric') } ext { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index 6066445e9d..f4ff239587 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -27,7 +27,7 @@ import java.util.Locale; * A helper class for periodically updating a {@link TextView} with debug information obtained from * a {@link SimpleExoPlayer}. */ -public class DebugTextViewHelper extends Player.DefaultEventListener implements Runnable { +public class DebugTextViewHelper implements Player.EventListener, Runnable { private static final int REFRESH_INTERVAL_MS = 1000; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java index 05c645d9c7..75c4f71b64 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java @@ -174,6 +174,12 @@ public class DefaultTimeBar extends View implements TimeBar { private static final long STOP_SCRUBBING_TIMEOUT_MS = 1000; private static final int DEFAULT_INCREMENT_COUNT = 20; + /** + * The name of the Android SDK view that most closely resembles this custom view. Used as the + * class name for accessibility. + */ + private static final String ACCESSIBILITY_CLASS_NAME = "android.widget.SeekBar"; + private final Rect seekBounds; private final Rect progressBar; private final Rect bufferedBar; @@ -184,7 +190,7 @@ public class DefaultTimeBar extends View implements TimeBar { private final Paint adMarkerPaint; private final Paint playedAdMarkerPaint; private final Paint scrubberPaint; - private final Drawable scrubberDrawable; + private final @Nullable Drawable scrubberDrawable; private final int barHeight; private final int touchTargetHeight; private final int adMarkerWidth; @@ -197,12 +203,12 @@ public class DefaultTimeBar extends View implements TimeBar { private final Formatter formatter; private final Runnable stopScrubbingRunnable; private final CopyOnWriteArraySet listeners; + private final int[] locationOnScreen; + private final Point touchPosition; private int keyCountIncrement; private long keyTimeIncrement; private int lastCoarseScrubXPosition; - private int[] locationOnScreen; - private Point touchPosition; private boolean scrubbing; private long scrubPosition; @@ -210,12 +216,12 @@ public class DefaultTimeBar extends View implements TimeBar { private long position; private long bufferedPosition; private int adGroupCount; - private long[] adGroupTimesMs; - private boolean[] playedAdGroups; + private @Nullable long[] adGroupTimesMs; + private @Nullable boolean[] playedAdGroups; - /** - * Creates a new time bar. - */ + /** Creates a new time bar. */ + // Suppress warnings due to usage of View methods in the constructor. + @SuppressWarnings("nullness:method.invocation.invalid") public DefaultTimeBar(Context context, AttributeSet attrs) { super(context, attrs); seekBounds = new Rect(); @@ -230,6 +236,8 @@ public class DefaultTimeBar extends View implements TimeBar { scrubberPaint = new Paint(); scrubberPaint.setAntiAlias(true); listeners = new CopyOnWriteArraySet<>(); + locationOnScreen = new int[2]; + touchPosition = new Point(); // Calculate the dimensions and paints for drawn elements. Resources res = context.getResources(); @@ -299,12 +307,7 @@ public class DefaultTimeBar extends View implements TimeBar { } formatBuilder = new StringBuilder(); formatter = new Formatter(formatBuilder, Locale.getDefault()); - stopScrubbingRunnable = new Runnable() { - @Override - public void run() { - stopScrubbing(false); - } - }; + stopScrubbingRunnable = () -> stopScrubbing(/* canceled= */ false); if (scrubberDrawable != null) { scrubberPadding = (scrubberDrawable.getMinimumWidth() + 1) / 2; } else { @@ -593,14 +596,14 @@ public class DefaultTimeBar extends View implements TimeBar { if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) { event.getText().add(getProgressText()); } - event.setClassName(DefaultTimeBar.class.getName()); + event.setClassName(ACCESSIBILITY_CLASS_NAME); } @TargetApi(21) @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(DefaultTimeBar.class.getCanonicalName()); + info.setClassName(ACCESSIBILITY_CLASS_NAME); info.setContentDescription(getProgressText()); if (duration <= 0) { return; @@ -616,7 +619,7 @@ public class DefaultTimeBar extends View implements TimeBar { @TargetApi(16) @Override - public boolean performAccessibilityAction(int action, Bundle args) { + public boolean performAccessibilityAction(int action, @Nullable Bundle args) { if (super.performAccessibilityAction(action, args)) { return true; } @@ -693,10 +696,6 @@ public class DefaultTimeBar extends View implements TimeBar { } private Point resolveRelativeTouchPosition(MotionEvent motionEvent) { - if (locationOnScreen == null) { - locationOnScreen = new int[2]; - touchPosition = new Point(); - } getLocationOnScreen(locationOnScreen); touchPosition.set( ((int) motionEvent.getRawX()) - locationOnScreen[0], @@ -736,6 +735,11 @@ public class DefaultTimeBar extends View implements TimeBar { if (scrubberBar.width() > 0) { canvas.drawRect(scrubberBar.left, barTop, scrubberBar.right, barBottom, playedPaint); } + if (adGroupCount == 0) { + return; + } + long[] adGroupTimesMs = Assertions.checkNotNull(this.adGroupTimesMs); + boolean[] playedAdGroups = Assertions.checkNotNull(this.playedAdGroups); int adMarkerOffset = adMarkerWidth / 2; for (int i = 0; i < adGroupCount; i++) { long adGroupTimeMs = Util.constrainValue(adGroupTimesMs[i], 0, duration); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTrackNameProvider.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTrackNameProvider.java index b36941e999..5d68387869 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTrackNameProvider.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTrackNameProvider.java @@ -43,11 +43,11 @@ public class DefaultTrackNameProvider implements TrackNameProvider { } else if (trackType == C.TRACK_TYPE_AUDIO) { trackName = joinWithSeparator( - buildLanguageString(format), + buildLabelString(format), buildAudioChannelString(format), buildBitrateString(format)); } else { - trackName = buildLanguageString(format); + trackName = buildLabelString(format); } return trackName.length() == 0 ? resources.getString(R.string.exo_track_unknown) : trackName; } @@ -87,7 +87,11 @@ public class DefaultTrackNameProvider implements TrackNameProvider { } } - private String buildLanguageString(Format format) { + private String buildLabelString(Format format) { + if (!TextUtils.isEmpty(format.label)) { + return format.label; + } + // Fall back to using the language. String language = format.language; return TextUtils.isEmpty(language) || C.LANGUAGE_UNDETERMINED.equals(language) ? "" diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java index 0a841fa38f..97832abfc7 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java @@ -55,10 +55,18 @@ public final class DownloadNotificationUtil { int downloadTaskCount = 0; boolean allDownloadPercentagesUnknown = true; boolean haveDownloadedBytes = false; + boolean haveDownloadTasks = false; + boolean haveRemoveTasks = false; for (TaskState taskState : taskStates) { - if (taskState.action.isRemoveAction || taskState.state != TaskState.STATE_STARTED) { + if (taskState.state != TaskState.STATE_STARTED + && taskState.state != TaskState.STATE_COMPLETED) { continue; } + if (taskState.action.isRemoveAction) { + haveRemoveTasks = true; + continue; + } + haveDownloadTasks = true; if (taskState.downloadPercentage != C.PERCENTAGE_UNSET) { allDownloadPercentagesUnknown = false; totalPercentage += taskState.downloadPercentage; @@ -67,18 +75,20 @@ public final class DownloadNotificationUtil { downloadTaskCount++; } - boolean haveDownloadTasks = downloadTaskCount > 0; int titleStringId = haveDownloadTasks ? R.string.exo_download_downloading - : (taskStates.length > 0 ? R.string.exo_download_removing : NULL_STRING_ID); + : (haveRemoveTasks ? R.string.exo_download_removing : NULL_STRING_ID); NotificationCompat.Builder notificationBuilder = newNotificationBuilder( context, smallIcon, channelId, contentIntent, message, titleStringId); - int progress = haveDownloadTasks ? (int) (totalPercentage / downloadTaskCount) : 0; - boolean indeterminate = - !haveDownloadTasks || (allDownloadPercentagesUnknown && haveDownloadedBytes); + int progress = 0; + boolean indeterminate = true; + if (haveDownloadTasks) { + progress = (int) (totalPercentage / downloadTaskCount); + indeterminate = allDownloadPercentagesUnknown && haveDownloadedBytes; + } notificationBuilder.setProgress(/* max= */ 100, progress, indeterminate); notificationBuilder.setOngoing(true); notificationBuilder.setShowWhen(false); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index 63c791d166..abe884ce53 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -25,6 +25,7 @@ import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; import android.widget.ImageView; @@ -87,7 +88,7 @@ import java.util.Locale; * below for more details. *

      *
    • Corresponding method: None - *
    • Default: {@code R.id.exo_player_control_view} + *
    • Default: {@code R.layout.exo_player_control_view} *
    * * @@ -715,7 +716,7 @@ public class PlayerControlView extends FrameLayout { long bufferedPosition = 0; long duration = 0; if (player != null) { - long currentWindowTimeBarOffsetUs = 0; + long currentWindowTimeBarOffsetMs = 0; long durationUs = 0; int adGroupCount = 0; Timeline timeline = player.getCurrentTimeline(); @@ -726,7 +727,7 @@ public class PlayerControlView extends FrameLayout { multiWindowTimeBar ? timeline.getWindowCount() - 1 : currentWindowIndex; for (int i = firstWindowIndex; i <= lastWindowIndex; i++) { if (i == currentWindowIndex) { - currentWindowTimeBarOffsetUs = durationUs; + currentWindowTimeBarOffsetMs = C.usToMs(durationUs); } timeline.getWindow(i, window); if (window.durationUs == C.TIME_UNSET) { @@ -762,15 +763,8 @@ public class PlayerControlView extends FrameLayout { } } duration = C.usToMs(durationUs); - position = C.usToMs(currentWindowTimeBarOffsetUs); - bufferedPosition = position; - if (player.isPlayingAd()) { - position += player.getContentPosition(); - bufferedPosition = position; - } else { - position += player.getCurrentPosition(); - bufferedPosition += player.getBufferedPosition(); - } + position = currentWindowTimeBarOffsetMs + player.getContentPosition(); + bufferedPosition = currentWindowTimeBarOffsetMs + player.getContentBufferedPosition(); if (timeBar != null) { int extraAdGroupCount = extraAdGroupTimesMs.length; int totalAdGroupCount = adGroupCount + extraAdGroupCount; @@ -952,6 +946,16 @@ public class PlayerControlView extends FrameLayout { removeCallbacks(hideAction); } + @Override + public final boolean dispatchTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + removeCallbacks(hideAction); + } else if (ev.getAction() == MotionEvent.ACTION_UP) { + hideAfterTimeout(); + } + return super.dispatchTouchEvent(ev); + } + @Override public boolean dispatchKeyEvent(KeyEvent event) { return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); @@ -1037,12 +1041,11 @@ public class PlayerControlView extends FrameLayout { return true; } - private final class ComponentListener extends Player.DefaultEventListener - implements TimeBar.OnScrubListener, OnClickListener { + private final class ComponentListener + implements Player.EventListener, TimeBar.OnScrubListener, OnClickListener { @Override public void onScrubStart(TimeBar timeBar, long position) { - removeCallbacks(hideAction); scrubbing = true; } @@ -1059,7 +1062,6 @@ public class PlayerControlView extends FrameLayout { if (!canceled && player != null) { seekToTimeBarPosition(position); } - hideAfterTimeout(); } @Override @@ -1088,7 +1090,7 @@ public class PlayerControlView extends FrameLayout { @Override public void onTimelineChanged( - Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { + Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { updateNavigation(); updateTimeBarMode(); updateProgress(); @@ -1123,7 +1125,6 @@ public class PlayerControlView extends FrameLayout { controlDispatcher.dispatchSetShuffleModeEnabled(player, !player.getShuffleModeEnabled()); } } - hideAfterTimeout(); } } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index 19051ba932..f3edacaebc 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -391,7 +391,7 @@ public class PlayerNotificationManager { customActions = customActionReceiver != null ? customActionReceiver.createCustomActions(context) - : Collections.emptyMap(); + : Collections.emptyMap(); for (String action : customActions.keySet()) { intentFilter.addAction(action); } @@ -460,7 +460,7 @@ public class PlayerNotificationManager { return; } this.fastForwardMs = fastForwardMs; - maybeUpdateNotification(); + invalidate(); } /** @@ -474,7 +474,7 @@ public class PlayerNotificationManager { return; } this.rewindMs = rewindMs; - maybeUpdateNotification(); + invalidate(); } /** @@ -485,7 +485,7 @@ public class PlayerNotificationManager { public final void setUseNavigationActions(boolean useNavigationActions) { if (this.useNavigationActions != useNavigationActions) { this.useNavigationActions = useNavigationActions; - maybeUpdateNotification(); + invalidate(); } } @@ -497,7 +497,7 @@ public class PlayerNotificationManager { public final void setUsePlayPauseActions(boolean usePlayPauseActions) { if (this.usePlayPauseActions != usePlayPauseActions) { this.usePlayPauseActions = usePlayPauseActions; - maybeUpdateNotification(); + invalidate(); } } @@ -520,7 +520,7 @@ public class PlayerNotificationManager { } else { stopPendingIntent = null; } - maybeUpdateNotification(); + invalidate(); } /** @@ -531,7 +531,7 @@ public class PlayerNotificationManager { public final void setMediaSessionToken(MediaSessionCompat.Token token) { if (!Util.areEqual(this.mediaSessionToken, token)) { mediaSessionToken = token; - maybeUpdateNotification(); + invalidate(); } } @@ -555,7 +555,7 @@ public class PlayerNotificationManager { default: throw new IllegalArgumentException(); } - maybeUpdateNotification(); + invalidate(); } /** @@ -569,7 +569,7 @@ public class PlayerNotificationManager { public final void setColorized(boolean colorized) { if (this.colorized != colorized) { this.colorized = colorized; - maybeUpdateNotification(); + invalidate(); } } @@ -583,7 +583,7 @@ public class PlayerNotificationManager { public final void setDefaults(int defaults) { if (this.defaults != defaults) { this.defaults = defaults; - maybeUpdateNotification(); + invalidate(); } } @@ -597,7 +597,7 @@ public class PlayerNotificationManager { public final void setColor(int color) { if (this.color != color) { this.color = color; - maybeUpdateNotification(); + invalidate(); } } @@ -613,7 +613,7 @@ public class PlayerNotificationManager { public final void setOngoing(boolean ongoing) { if (this.ongoing != ongoing) { this.ongoing = ongoing; - maybeUpdateNotification(); + invalidate(); } } @@ -642,7 +642,7 @@ public class PlayerNotificationManager { default: throw new IllegalArgumentException(); } - maybeUpdateNotification(); + invalidate(); } /** @@ -655,7 +655,7 @@ public class PlayerNotificationManager { public final void setSmallIcon(@DrawableRes int smallIconResourceId) { if (this.smallIconResourceId != smallIconResourceId) { this.smallIconResourceId = smallIconResourceId; - maybeUpdateNotification(); + invalidate(); } } @@ -669,7 +669,7 @@ public class PlayerNotificationManager { public final void setUseChronometer(boolean useChronometer) { if (this.useChronometer != useChronometer) { this.useChronometer = useChronometer; - maybeUpdateNotification(); + invalidate(); } } @@ -696,7 +696,14 @@ public class PlayerNotificationManager { default: throw new IllegalStateException(); } - maybeUpdateNotification(); + invalidate(); + } + + /** Forces an update of the notification if already started. */ + public void invalidate() { + if (isNotificationStarted && player != null) { + updateNotification(null); + } } @RequiresNonNull("player") @@ -719,12 +726,6 @@ public class PlayerNotificationManager { } } - private void maybeUpdateNotification() { - if (isNotificationStarted && player != null) { - updateNotification(null); - } - } - private void stopNotification() { if (isNotificationStarted) { notificationManager.cancel(notificationId); @@ -744,7 +745,6 @@ public class PlayerNotificationManager { * @return The {@link Notification} which has been built. */ protected Notification createNotification(Player player, @Nullable Bitmap largeIcon) { - boolean isPlayingAd = player.isPlayingAd(); NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId); List actionNames = getActions(player); for (int i = 0; i < actionNames.size(); i++) { @@ -759,18 +759,18 @@ public class PlayerNotificationManager { } // Create a media style notification. MediaStyle mediaStyle = new MediaStyle(); - builder.setStyle(mediaStyle); if (mediaSessionToken != null) { mediaStyle.setMediaSession(mediaSessionToken); } - mediaStyle.setShowActionsInCompactView(getActionIndicesForCompactView(player)); + mediaStyle.setShowActionsInCompactView(getActionIndicesForCompactView(actionNames, player)); // Configure stop action (eg. when user dismisses the notification when !isOngoing). - boolean useStopAction = stopAction != null && !isPlayingAd; + boolean useStopAction = stopAction != null; mediaStyle.setShowCancelButton(useStopAction); if (useStopAction && stopPendingIntent != null) { builder.setDeleteIntent(stopPendingIntent); mediaStyle.setCancelButtonIntent(stopPendingIntent); } + builder.setStyle(mediaStyle); // Set notification properties from getters. builder .setBadgeIconType(badgeIconType) @@ -782,6 +782,7 @@ public class PlayerNotificationManager { .setPriority(priority) .setDefaults(defaults); if (useChronometer + && !player.isPlayingAd() && !player.isCurrentWindowDynamic() && player.getPlayWhenReady() && player.getPlaybackState() == Player.STATE_READY) { @@ -830,33 +831,36 @@ public class PlayerNotificationManager { * name is ignored. */ protected List getActions(Player player) { + boolean isPlayingAd = player.isPlayingAd(); List stringActions = new ArrayList<>(); - if (!player.isPlayingAd()) { + if (!isPlayingAd) { if (useNavigationActions) { stringActions.add(ACTION_PREVIOUS); } if (rewindMs > 0) { stringActions.add(ACTION_REWIND); } - if (usePlayPauseActions) { - if (player.getPlayWhenReady()) { - stringActions.add(ACTION_PAUSE); - } else { - stringActions.add(ACTION_PLAY); - } + } + if (usePlayPauseActions) { + if (player.getPlayWhenReady()) { + stringActions.add(ACTION_PAUSE); + } else { + stringActions.add(ACTION_PLAY); } + } + if (!isPlayingAd) { if (fastForwardMs > 0) { stringActions.add(ACTION_FAST_FORWARD); } if (useNavigationActions && player.getNextWindowIndex() != C.INDEX_UNSET) { stringActions.add(ACTION_NEXT); } - if (customActionReceiver != null) { - stringActions.addAll(customActionReceiver.getCustomActions(player)); - } - if (ACTION_STOP.equals(stopAction)) { - stringActions.add(stopAction); - } + } + if (customActionReceiver != null) { + stringActions.addAll(customActionReceiver.getCustomActions(player)); + } + if (ACTION_STOP.equals(stopAction)) { + stringActions.add(stopAction); } return stringActions; } @@ -864,18 +868,18 @@ public class PlayerNotificationManager { /** * Gets an array with the indices of the buttons to be shown in compact mode. * - *

    This method can be overridden. The indices must refer to the list of actions returned by - * {@link #getActions(Player)}. + *

    This method can be overridden. The indices must refer to the list of actions passed as the + * first parameter. * + * @param actionNames The names of the actions included in the notification. * @param player The player for which state to build a notification. */ - protected int[] getActionIndicesForCompactView(Player player) { - if (!usePlayPauseActions) { - return new int[0]; - } - int actionIndex = useNavigationActions ? 1 : 0; - actionIndex += fastForwardMs > 0 ? 1 : 0; - return new int[] {actionIndex}; + protected int[] getActionIndicesForCompactView(List actionNames, Player player) { + int pauseActionIndex = actionNames.indexOf(ACTION_PAUSE); + int playActionIndex = actionNames.indexOf(ACTION_PLAY); + return pauseActionIndex != -1 + ? new int[] {pauseActionIndex} + : (playActionIndex != -1 ? new int[] {playActionIndex} : new int[0]); } private static Map createPlaybackActions(Context context) { @@ -936,7 +940,7 @@ public class PlayerNotificationManager { return actions; } - private class PlayerListener extends Player.DefaultEventListener { + private class PlayerListener implements Player.EventListener { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { @@ -949,7 +953,7 @@ public class PlayerNotificationManager { } @Override - public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { + public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) { if (player == null || player.getPlaybackState() == Player.STATE_IDLE) { return; } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index a7aa48c0db..99f38b4c40 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ui; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; @@ -24,12 +25,17 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; +import android.view.Surface; import android.view.SurfaceView; import android.view.TextureView; import android.view.View; @@ -44,6 +50,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.VideoComponent; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -52,11 +59,14 @@ import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode; +import com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.util.RepeatModeUtil; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoListener; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.List; /** @@ -79,7 +89,7 @@ import java.util.List; *

  • {@code default_artwork} - Default artwork to use if no artwork available in audio * streams. *
      - *
    • Corresponding method: {@link #setDefaultArtwork(Bitmap)} + *
    • Corresponding method: {@link #setDefaultArtwork(Drawable)} *
    • Default: {@code null} *
    *
  • {@code use_controller} - Whether the playback controls can be shown. @@ -106,10 +116,10 @@ import java.util.List; *
  • Default: {@code true} * *
  • {@code show_buffering} - Whether the buffering spinner is displayed when the player - * is buffering. + * is buffering. Valid values are {@code never}, {@code when_playing} and {@code always}. *
      - *
    • Corresponding method: {@link #setShowBuffering(boolean)} - *
    • Default: {@code false} + *
    • Corresponding method: {@link #setShowBuffering(int)} + *
    • Default: {@code never} *
    *
  • {@code resize_mode} - Controls how video and album art is resized within the view. * Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height} and {@code fill}. @@ -118,11 +128,11 @@ import java.util.List; *
  • Default: {@code fit} * *
  • {@code surface_type} - The type of surface view used for video playbacks. Valid - * values are {@code surface_view}, {@code texture_view} and {@code none}. Using {@code none} - * is recommended for audio only applications, since creating the surface can be expensive. - * Using {@code surface_view} is recommended for video applications. Note, TextureView can - * only be used in a hardware accelerated window. When rendered in software, TextureView will - * draw nothing. + * values are {@code surface_view}, {@code texture_view}, {@code spherical_view} and {@code + * none}. Using {@code none} is recommended for audio only applications, since creating the + * surface can be expensive. Using {@code surface_view} is recommended for video applications. + * Note, TextureView can only be used in a hardware accelerated window. When rendered in + * software, TextureView will draw nothing. *
      *
    • Corresponding method: None *
    • Default: {@code surface_view} @@ -143,13 +153,13 @@ import java.util.List; * for more details. *
        *
      • Corresponding method: None - *
      • Default: {@code R.id.exo_player_view} + *
      • Default: {@code R.layout.exo_player_view} *
      *
    • {@code controller_layout_id} - Specifies the id of the layout resource to be * inflated by the child {@link PlayerControlView}. See below for more details. *
        *
      • Corresponding method: None - *
      • Default: {@code R.id.exo_player_control_view} + *
      • Default: {@code R.layout.exo_player_control_view} *
      *
    • All attributes that can be set on a {@link PlayerControlView} can also be set on a * PlayerView, and will be propagated to the inflated {@link PlayerControlView} unless the @@ -231,6 +241,24 @@ public class PlayerView extends FrameLayout { private static final int SURFACE_TYPE_NONE = 0; private static final int SURFACE_TYPE_SURFACE_VIEW = 1; private static final int SURFACE_TYPE_TEXTURE_VIEW = 2; + private static final int SURFACE_TYPE_MONO360_VIEW = 3; + + /** Determines when the buffering view is shown. */ + @IntDef({SHOW_BUFFERING_NEVER, SHOW_BUFFERING_WHEN_PLAYING, SHOW_BUFFERING_ALWAYS}) + @Retention(RetentionPolicy.SOURCE) + public @interface ShowBuffering {} + /** The buffering view is never shown. */ + public static final int SHOW_BUFFERING_NEVER = 0; + /** + * The buffering view is shown when the player is in the {@link Player#STATE_BUFFERING buffering} + * state and {@link Player#getPlayWhenReady() playWhenReady} is {@code true}. + */ + public static final int SHOW_BUFFERING_WHEN_PLAYING = 1; + /** + * The buffering view is always shown when the player is in the {@link Player#STATE_BUFFERING + * buffering} state. + */ + public static final int SHOW_BUFFERING_ALWAYS = 2; private final AspectRatioFrameLayout contentFrame; private final View shutterView; @@ -246,8 +274,8 @@ public class PlayerView extends FrameLayout { private Player player; private boolean useController; private boolean useArtwork; - private Bitmap defaultArtwork; - private boolean showBuffering; + private @Nullable Drawable defaultArtwork; + private @ShowBuffering int showBuffering; private boolean keepContentOnPlayerReset; private @Nullable ErrorMessageProvider errorMessageProvider; private @Nullable CharSequence customErrorMessage; @@ -301,7 +329,7 @@ public class PlayerView extends FrameLayout { boolean controllerHideOnTouch = true; boolean controllerAutoShow = true; boolean controllerHideDuringAds = true; - boolean showBuffering = false; + int showBuffering = SHOW_BUFFERING_NEVER; if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlayerView, 0, 0); try { @@ -319,7 +347,7 @@ public class PlayerView extends FrameLayout { controllerHideOnTouch = a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch); controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow); - showBuffering = a.getBoolean(R.styleable.PlayerView_show_buffering, showBuffering); + showBuffering = a.getInteger(R.styleable.PlayerView_show_buffering, showBuffering); keepContentOnPlayerReset = a.getBoolean( R.styleable.PlayerView_keep_content_on_player_reset, keepContentOnPlayerReset); @@ -351,10 +379,20 @@ public class PlayerView extends FrameLayout { ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - surfaceView = - surfaceType == SURFACE_TYPE_TEXTURE_VIEW - ? new TextureView(context) - : new SurfaceView(context); + switch (surfaceType) { + case SURFACE_TYPE_TEXTURE_VIEW: + surfaceView = new TextureView(context); + break; + case SURFACE_TYPE_MONO360_VIEW: + Assertions.checkState(Util.SDK_INT >= 15); + SphericalSurfaceView sphericalSurfaceView = new SphericalSurfaceView(context); + sphericalSurfaceView.setSurfaceListener(componentListener); + surfaceView = sphericalSurfaceView; + break; + default: + surfaceView = new SurfaceView(context); + break; + } surfaceView.setLayoutParams(params); contentFrame.addView(surfaceView, 0); } else { @@ -368,7 +406,7 @@ public class PlayerView extends FrameLayout { artworkView = findViewById(R.id.exo_artwork); this.useArtwork = useArtwork && artworkView != null; if (defaultArtworkId != 0) { - defaultArtwork = BitmapFactory.decodeResource(context.getResources(), defaultArtworkId); + defaultArtwork = ContextCompat.getDrawable(getContext(), defaultArtworkId); } // Subtitle view. @@ -469,6 +507,8 @@ public class PlayerView extends FrameLayout { oldVideoComponent.removeVideoListener(componentListener); if (surfaceView instanceof TextureView) { oldVideoComponent.clearVideoTextureView((TextureView) surfaceView); + } else if (surfaceView instanceof SphericalSurfaceView) { + oldVideoComponent.clearVideoSurface(((SphericalSurfaceView) surfaceView).getSurface()); } else if (surfaceView instanceof SurfaceView) { oldVideoComponent.clearVideoSurfaceView((SurfaceView) surfaceView); } @@ -493,6 +533,8 @@ public class PlayerView extends FrameLayout { if (newVideoComponent != null) { if (surfaceView instanceof TextureView) { newVideoComponent.setVideoTextureView((TextureView) surfaceView); + } else if (surfaceView instanceof SphericalSurfaceView) { + newVideoComponent.setVideoSurface(((SphericalSurfaceView) surfaceView).getSurface()); } else if (surfaceView instanceof SurfaceView) { newVideoComponent.setVideoSurfaceView((SurfaceView) surfaceView); } @@ -553,7 +595,7 @@ public class PlayerView extends FrameLayout { } /** Returns the default artwork to display. */ - public Bitmap getDefaultArtwork() { + public Drawable getDefaultArtwork() { return defaultArtwork; } @@ -562,8 +604,21 @@ public class PlayerView extends FrameLayout { * present in the media. * * @param defaultArtwork the default artwork to display. + * @deprecated use (@link {@link #setDefaultArtwork(Drawable)} instead. */ - public void setDefaultArtwork(Bitmap defaultArtwork) { + @Deprecated + public void setDefaultArtwork(@Nullable Bitmap defaultArtwork) { + setDefaultArtwork( + defaultArtwork == null ? null : new BitmapDrawable(getResources(), defaultArtwork)); + } + + /** + * Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is + * present in the media. + * + * @param defaultArtwork the default artwork to display + */ + public void setDefaultArtwork(@Nullable Drawable defaultArtwork) { if (this.defaultArtwork != defaultArtwork) { this.defaultArtwork = defaultArtwork; updateForCurrentTrackSelections(/* isNewPlayer= */ false); @@ -636,9 +691,23 @@ public class PlayerView extends FrameLayout { * Sets whether a buffering spinner is displayed when the player is in the buffering state. The * buffering spinner is not displayed by default. * - * @param showBuffering Whether the buffering icon is displayer + * @deprecated Use {@link #setShowBuffering(int)} + * @param showBuffering Whether the buffering icon is displayed */ + @Deprecated public void setShowBuffering(boolean showBuffering) { + setShowBuffering(showBuffering ? SHOW_BUFFERING_WHEN_PLAYING : SHOW_BUFFERING_NEVER); + } + + /** + * Sets whether a buffering spinner is displayed when the player is in the buffering state. The + * buffering spinner is not displayed by default. + * + * @param showBuffering The mode that defines when the buffering spinner is displayed. One of + * {@link #SHOW_BUFFERING_NEVER}, {@link #SHOW_BUFFERING_WHEN_PLAYING} and + * {@link #SHOW_BUFFERING_ALWAYS}. + */ + public void setShowBuffering(@ShowBuffering int showBuffering) { if (this.showBuffering != showBuffering) { this.showBuffering = showBuffering; updateBuffering(); @@ -681,8 +750,12 @@ public class PlayerView extends FrameLayout { } boolean isDpadWhenControlHidden = isDpadKey(event.getKeyCode()) && useController && !controller.isVisible(); - maybeShowController(true); - return isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); + boolean handled = + isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); + if (handled) { + maybeShowController(true); + } + return handled; } /** @@ -696,6 +769,11 @@ public class PlayerView extends FrameLayout { return useController && controller.dispatchMediaKeyEvent(event); } + /** Returns whether the controller is currently visible. */ + public boolean isControllerVisible() { + return controller != null && controller.isVisible(); + } + /** * Shows the playback controls. Does nothing if playback controls are disabled. * @@ -904,10 +982,12 @@ public class PlayerView extends FrameLayout { *
    • {@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code * surface_view}. *
    • {@link TextureView} if {@code surface_type} is {@code texture_view}. + *
    • {@link SphericalSurfaceView} if {@code surface_type} is {@code spherical_view}. *
    • {@code null} if {@code surface_type} is {@code none}. *
    * - * @return The {@link SurfaceView}, {@link TextureView} or {@code null}. + * @return The {@link SurfaceView}, {@link TextureView}, {@link SphericalSurfaceView} or {@code + * null}. */ public View getVideoSurfaceView() { return surfaceView; @@ -956,6 +1036,32 @@ public class PlayerView extends FrameLayout { return true; } + /** + * Should be called when the player is visible to the user and if {@code surface_type} is {@code + * spherical_view}. It is the counterpart to {@link #onPause()}. + * + *

    This method should typically be called in {@link Activity#onStart()}, or {@link + * Activity#onResume()} for API versions <= 23. + */ + public void onResume() { + if (surfaceView instanceof SphericalSurfaceView) { + ((SphericalSurfaceView) surfaceView).onResume(); + } + } + + /** + * Should be called when the player is no longer visible to the user and if {@code surface_type} + * is {@code spherical_view}. It is the counterpart to {@link #onResume()}. + * + *

    This method should typically be called in {@link Activity#onStop()}, or {@link + * Activity#onPause()} for API versions <= 23. + */ + public void onPause() { + if (surfaceView instanceof SphericalSurfaceView) { + ((SphericalSurfaceView) surfaceView).onPause(); + } + } + /** Shows the playback controls, but only if forced or shown indefinitely. */ private void maybeShowController(boolean isForced) { if (isPlayingAd() && controllerHideDuringAds) { @@ -1032,7 +1138,7 @@ public class PlayerView extends FrameLayout { } } } - if (setArtworkFromBitmap(defaultArtwork)) { + if (setDrawableArtwork(defaultArtwork)) { return; } } @@ -1046,21 +1152,21 @@ public class PlayerView extends FrameLayout { if (metadataEntry instanceof ApicFrame) { byte[] bitmapData = ((ApicFrame) metadataEntry).pictureData; Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length); - return setArtworkFromBitmap(bitmap); + return setDrawableArtwork(new BitmapDrawable(getResources(), bitmap)); } } return false; } - private boolean setArtworkFromBitmap(Bitmap bitmap) { - if (bitmap != null) { - int bitmapWidth = bitmap.getWidth(); - int bitmapHeight = bitmap.getHeight(); - if (bitmapWidth > 0 && bitmapHeight > 0) { + private boolean setDrawableArtwork(@Nullable Drawable drawable) { + if (drawable != null) { + int drawableWidth = drawable.getIntrinsicWidth(); + int drawableHeight = drawable.getIntrinsicHeight(); + if (drawableWidth > 0 && drawableHeight > 0) { if (contentFrame != null) { - contentFrame.setAspectRatio((float) bitmapWidth / bitmapHeight); + contentFrame.setAspectRatio((float) drawableWidth / drawableHeight); } - artworkView.setImageBitmap(bitmap); + artworkView.setImageDrawable(drawable); artworkView.setVisibility(VISIBLE); return true; } @@ -1084,10 +1190,10 @@ public class PlayerView extends FrameLayout { private void updateBuffering() { if (bufferingView != null) { boolean showBufferingSpinner = - showBuffering - && player != null + player != null && player.getPlaybackState() == Player.STATE_BUFFERING - && player.getPlayWhenReady(); + && (showBuffering == SHOW_BUFFERING_ALWAYS + || (showBuffering == SHOW_BUFFERING_WHEN_PLAYING && player.getPlayWhenReady())); bufferingView.setVisibility(showBufferingSpinner ? View.VISIBLE : View.GONE); } } @@ -1170,8 +1276,12 @@ public class PlayerView extends FrameLayout { || keyCode == KeyEvent.KEYCODE_DPAD_CENTER; } - private final class ComponentListener extends Player.DefaultEventListener - implements TextOutput, VideoListener, OnLayoutChangeListener { + private final class ComponentListener + implements Player.EventListener, + TextOutput, + VideoListener, + OnLayoutChangeListener, + SphericalSurfaceView.SurfaceListener { // TextOutput implementation @@ -1210,6 +1320,8 @@ public class PlayerView extends FrameLayout { surfaceView.addOnLayoutChangeListener(this); } applyTextureViewRotation((TextureView) surfaceView, textureViewRotation); + } else if (surfaceView instanceof SphericalSurfaceView) { + videoAspectRatio = 0; } contentFrame.setAspectRatio(videoAspectRatio); @@ -1262,5 +1374,17 @@ public class PlayerView extends FrameLayout { int oldBottom) { applyTextureViewRotation((TextureView) view, textureViewRotation); } + + // SphericalSurfaceView.SurfaceTextureListener implementation + + @Override + public void surfaceChanged(@Nullable Surface surface) { + if (player != null) { + VideoComponent videoComponent = player.getVideoComponent(); + if (videoComponent != null) { + videoComponent.setVideoSurface(surface); + } + } + } } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index c5d264b310..db46ee4912 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -25,13 +25,14 @@ import android.graphics.Paint; import android.graphics.Paint.Join; import android.graphics.Paint.Style; import android.graphics.Rect; -import android.graphics.RectF; import android.text.Layout.Alignment; import android.text.SpannableStringBuilder; +import android.text.Spanned; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; +import android.text.style.BackgroundColorSpan; import android.text.style.RelativeSizeSpan; import android.util.DisplayMetrics; import android.util.Log; @@ -51,13 +52,7 @@ import com.google.android.exoplayer2.util.Util; */ private static final float INNER_PADDING_RATIO = 0.125f; - /** - * Temporary rectangle used for computing line bounds. - */ - private final RectF lineBounds = new RectF(); - // Styled dimensions. - private final float cornerRadius; private final float outlineWidth; private final float shadowRadius; private final float shadowOffset; @@ -89,7 +84,8 @@ import com.google.android.exoplayer2.util.Util; private int edgeColor; @CaptionStyleCompat.EdgeType private int edgeType; - private float textSizePx; + private float defaultTextSizePx; + private float cueTextSizePx; private float bottomPaddingFraction; private int parentLeft; private int parentTop; @@ -114,7 +110,6 @@ import com.google.android.exoplayer2.util.Util; Resources resources = context.getResources(); DisplayMetrics displayMetrics = resources.getDisplayMetrics(); int twoDpInPx = Math.round((2f * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT); - cornerRadius = twoDpInPx; outlineWidth = twoDpInPx; shadowRadius = twoDpInPx; shadowOffset = twoDpInPx; @@ -130,8 +125,8 @@ import com.google.android.exoplayer2.util.Util; /** * Draws the provided {@link Cue} into a canvas with the specified styling. - *

    - * A call to this method is able to use cached results of calculations made during the previous + * + *

    A call to this method is able to use cached results of calculations made during the previous * call, and so an instance of this class is able to optimize repeated calls to this method in * which the same parameters are passed. * @@ -140,7 +135,8 @@ import com.google.android.exoplayer2.util.Util; * @param applyEmbeddedFontSizes If {@code applyEmbeddedStyles} is true, defines whether font * sizes embedded within the cue should be applied. Otherwise, it is ignored. * @param style The style to use when drawing the cue text. - * @param textSizePx The text size to use when drawing the cue text, in pixels. + * @param defaultTextSizePx The default text size to use when drawing the text, in pixels. + * @param cueTextSizePx The embedded text size of this cue, in pixels. * @param bottomPaddingFraction The bottom padding fraction to apply when {@link Cue#line} is * {@link Cue#DIMEN_UNSET}, as a fraction of the viewport height * @param canvas The canvas into which to draw. @@ -149,9 +145,19 @@ import com.google.android.exoplayer2.util.Util; * @param cueBoxRight The right position of the enclosing cue box. * @param cueBoxBottom The bottom position of the enclosing cue box. */ - public void draw(Cue cue, boolean applyEmbeddedStyles, boolean applyEmbeddedFontSizes, - CaptionStyleCompat style, float textSizePx, float bottomPaddingFraction, Canvas canvas, - int cueBoxLeft, int cueBoxTop, int cueBoxRight, int cueBoxBottom) { + public void draw( + Cue cue, + boolean applyEmbeddedStyles, + boolean applyEmbeddedFontSizes, + CaptionStyleCompat style, + float defaultTextSizePx, + float cueTextSizePx, + float bottomPaddingFraction, + Canvas canvas, + int cueBoxLeft, + int cueBoxTop, + int cueBoxRight, + int cueBoxBottom) { boolean isTextCue = cue.bitmap == null; int windowColor = Color.BLACK; if (isTextCue) { @@ -180,7 +186,8 @@ import com.google.android.exoplayer2.util.Util; && this.edgeType == style.edgeType && this.edgeColor == style.edgeColor && Util.areEqual(this.textPaint.getTypeface(), style.typeface) - && this.textSizePx == textSizePx + && this.defaultTextSizePx == defaultTextSizePx + && this.cueTextSizePx == cueTextSizePx && this.bottomPaddingFraction == bottomPaddingFraction && this.parentLeft == cueBoxLeft && this.parentTop == cueBoxTop @@ -209,7 +216,8 @@ import com.google.android.exoplayer2.util.Util; this.edgeType = style.edgeType; this.edgeColor = style.edgeColor; this.textPaint.setTypeface(style.typeface); - this.textSizePx = textSizePx; + this.defaultTextSizePx = defaultTextSizePx; + this.cueTextSizePx = cueTextSizePx; this.bottomPaddingFraction = bottomPaddingFraction; this.parentLeft = cueBoxLeft; this.parentTop = cueBoxTop; @@ -228,8 +236,8 @@ import com.google.android.exoplayer2.util.Util; int parentWidth = parentRight - parentLeft; int parentHeight = parentBottom - parentTop; - textPaint.setTextSize(textSizePx); - int textPaddingX = (int) (textSizePx * INNER_PADDING_RATIO + 0.5f); + textPaint.setTextSize(defaultTextSizePx); + int textPaddingX = (int) (defaultTextSizePx * INNER_PADDING_RATIO + 0.5f); int availableWidth = parentWidth - textPaddingX * 2; if (cueSize != Cue.DIMEN_UNSET) { @@ -240,14 +248,12 @@ import com.google.android.exoplayer2.util.Util; return; } + CharSequence cueText = this.cueText; // Remove embedded styling or font size if requested. - CharSequence cueText; - if (applyEmbeddedFontSizes && applyEmbeddedStyles) { - cueText = this.cueText; - } else if (!applyEmbeddedStyles) { - cueText = this.cueText.toString(); // Equivalent to erasing all spans. - } else { - SpannableStringBuilder newCueText = new SpannableStringBuilder(this.cueText); + if (!applyEmbeddedStyles) { + cueText = cueText.toString(); // Equivalent to erasing all spans. + } else if (!applyEmbeddedFontSizes) { + SpannableStringBuilder newCueText = new SpannableStringBuilder(cueText); int cueLength = newCueText.length(); AbsoluteSizeSpan[] absSpans = newCueText.getSpans(0, cueLength, AbsoluteSizeSpan.class); RelativeSizeSpan[] relSpans = newCueText.getSpans(0, cueLength, RelativeSizeSpan.class); @@ -258,6 +264,26 @@ import com.google.android.exoplayer2.util.Util; newCueText.removeSpan(relSpan); } cueText = newCueText; + } else { + // Apply embedded styles & font size. + if (cueTextSizePx > 0) { + // Use a SpannableStringBuilder encompassing the whole cue text to apply the default + // cueTextSizePx. + SpannableStringBuilder newCueText = new SpannableStringBuilder(cueText); + newCueText.setSpan( + new AbsoluteSizeSpan((int) cueTextSizePx), + /* start= */ 0, + /* end= */ newCueText.length(), + Spanned.SPAN_PRIORITY); + cueText = newCueText; + } + } + + if (Color.alpha(backgroundColor) > 0) { + SpannableStringBuilder newCueText = new SpannableStringBuilder(cueText); + newCueText.setSpan( + new BackgroundColorSpan(backgroundColor), 0, newCueText.length(), Spanned.SPAN_PRIORITY); + cueText = newCueText; } Alignment textAlignment = cueTextAlignment == null ? Alignment.ALIGN_CENTER : cueTextAlignment; @@ -367,30 +393,6 @@ import com.google.android.exoplayer2.util.Util; paint); } - if (Color.alpha(backgroundColor) > 0) { - paint.setColor(backgroundColor); - float previousBottom = layout.getLineTop(0); - int lineCount = layout.getLineCount(); - for (int i = 0; i < lineCount; i++) { - float lineTextBoundLeft = layout.getLineLeft(i); - float lineTextBoundRight = layout.getLineRight(i); - lineBounds.left = lineTextBoundLeft - textPaddingX; - lineBounds.right = lineTextBoundRight + textPaddingX; - lineBounds.top = previousBottom; - lineBounds.bottom = layout.getLineBottom(i); - previousBottom = lineBounds.bottom; - float lineTextWidth = lineTextBoundRight - lineTextBoundLeft; - if (lineTextWidth > 0) { - // Do not draw a line's background color if it has no text. - // For some reason, calculating the width manually is more reliable than - // layout.getLineWidth(). - // Sometimes, lineTextBoundRight == lineTextBoundLeft, and layout.getLineWidth() still - // returns non-zero value. - canvas.drawRoundRect(lineBounds, cornerRadius, cornerRadius, paint); - } - } - } - if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) { textPaint.setStrokeJoin(Join.ROUND); textPaint.setStrokeWidth(outlineWidth); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index 4dbd4d5fec..7426671041 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -206,8 +206,10 @@ public final class SubtitleView extends View implements TextOutput { * {@link CaptioningManager#getUserStyle()}, or to a default style before API level 19. */ public void setUserDefaultStyle() { - setStyle(Util.SDK_INT >= 19 && !isInEditMode() - ? getUserCaptionStyleV19() : CaptionStyleCompat.DEFAULT); + setStyle( + Util.SDK_INT >= 19 && isCaptionManagerEnabled() && !isInEditMode() + ? getUserCaptionStyleV19() + : CaptionStyleCompat.DEFAULT); } /** @@ -251,7 +253,7 @@ public final class SubtitleView extends View implements TextOutput { // Calculate the bounds after padding is taken into account. int left = getLeft() + getPaddingLeft(); int top = rawTop + getPaddingTop(); - int right = getRight() + getPaddingRight(); + int right = getRight() - getPaddingRight(); int bottom = rawBottom - getPaddingBottom(); if (bottom <= top || right <= left) { // No space to draw subtitles. @@ -269,15 +271,15 @@ public final class SubtitleView extends View implements TextOutput { for (int i = 0; i < cueCount; i++) { Cue cue = cues.get(i); - float textSizePx = - resolveTextSizeForCue(cue, rawViewHeight, viewHeightMinusPadding, defaultViewTextSizePx); + float cueTextSizePx = resolveCueTextSize(cue, rawViewHeight, viewHeightMinusPadding); SubtitlePainter painter = painters.get(i); painter.draw( cue, applyEmbeddedStyles, applyEmbeddedFontSizes, style, - textSizePx, + defaultViewTextSizePx, + cueTextSizePx, bottomPaddingFraction, canvas, left, @@ -287,14 +289,13 @@ public final class SubtitleView extends View implements TextOutput { } } - private float resolveTextSizeForCue( - Cue cue, int rawViewHeight, int viewHeightMinusPadding, float defaultViewTextSizePx) { + private float resolveCueTextSize(Cue cue, int rawViewHeight, int viewHeightMinusPadding) { if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) { - return defaultViewTextSizePx; + return 0; } float defaultCueTextSizePx = resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding); - return defaultCueTextSizePx > 0 ? defaultCueTextSizePx : defaultViewTextSizePx; + return Math.max(defaultCueTextSizePx, 0); } private float resolveTextSize( @@ -315,6 +316,13 @@ public final class SubtitleView extends View implements TextOutput { } } + @TargetApi(19) + private boolean isCaptionManagerEnabled() { + CaptioningManager captioningManager = + (CaptioningManager) getContext().getSystemService(Context.CAPTIONING_SERVICE); + return captioningManager.isEnabled(); + } + @TargetApi(19) private float getUserCaptionFontScaleV19() { CaptioningManager captioningManager = diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java index be0babf5a8..fe5d5cbbc5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java @@ -203,7 +203,9 @@ public class TrackSelectionView extends LinearLayout { removeViewAt(i); } - if (trackSelector == null) { + MappingTrackSelector.MappedTrackInfo trackInfo = + trackSelector == null ? null : trackSelector.getCurrentMappedTrackInfo(); + if (trackSelector == null || trackInfo == null) { // The view is not initialized. disableView.setEnabled(false); defaultView.setEnabled(false); @@ -212,7 +214,6 @@ public class TrackSelectionView extends LinearLayout { disableView.setEnabled(true); defaultView.setEnabled(true); - MappingTrackSelector.MappedTrackInfo trackInfo = trackSelector.getCurrentMappedTrackInfo(); trackGroups = trackInfo.getTrackGroups(rendererIndex); DefaultTrackSelector.Parameters parameters = trackSelector.getParameters(); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/GlUtil.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/GlUtil.java new file mode 100644 index 0000000000..c962ad78c0 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/GlUtil.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ui.spherical; + +import static android.opengl.GLU.gluErrorString; + +import android.annotation.TargetApi; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import android.text.TextUtils; +import android.util.Log; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +/** GL utility methods. */ +/*package*/ final class GlUtil { + private static final String TAG = "Spherical.Utils"; + + /** Class only contains static methods. */ + private GlUtil() {} + + /** + * If there is an OpenGl error, logs the error and if {@link + * ExoPlayerLibraryInfo#GL_ASSERTIONS_ENABLED} is true throws a {@link RuntimeException}. + */ + public static void checkGlError() { + int error = GLES20.glGetError(); + int lastError; + if (error != GLES20.GL_NO_ERROR) { + do { + lastError = error; + Log.e(TAG, "glError " + gluErrorString(lastError)); + error = GLES20.glGetError(); + } while (error != GLES20.GL_NO_ERROR); + + if (ExoPlayerLibraryInfo.GL_ASSERTIONS_ENABLED) { + throw new RuntimeException("glError " + gluErrorString(lastError)); + } + } + } + + /** + * Builds a GL shader program from vertex & fragment shader code. The vertex and fragment shaders + * are passed as arrays of strings in order to make debugging compilation issues easier. + * + * @param vertexCode GLES20 vertex shader program. + * @param fragmentCode GLES20 fragment shader program. + * @return GLES20 program id. + */ + public static int compileProgram(String[] vertexCode, String[] fragmentCode) { + checkGlError(); + // prepare shaders and OpenGL program + int vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER); + GLES20.glShaderSource(vertexShader, TextUtils.join("\n", vertexCode)); + GLES20.glCompileShader(vertexShader); + checkGlError(); + + int fragmentShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER); + GLES20.glShaderSource(fragmentShader, TextUtils.join("\n", fragmentCode)); + GLES20.glCompileShader(fragmentShader); + checkGlError(); + + int program = GLES20.glCreateProgram(); + GLES20.glAttachShader(program, vertexShader); + GLES20.glAttachShader(program, fragmentShader); + + // Link and check for errors. + GLES20.glLinkProgram(program); + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] != GLES20.GL_TRUE) { + String errorMsg = "Unable to link shader program: \n" + GLES20.glGetProgramInfoLog(program); + Log.e(TAG, errorMsg); + if (ExoPlayerLibraryInfo.GL_ASSERTIONS_ENABLED) { + throw new RuntimeException(errorMsg); + } + } + checkGlError(); + + return program; + } + + /** Allocates a FloatBuffer with the given data. */ + public static FloatBuffer createBuffer(float[] data) { + ByteBuffer bb = ByteBuffer.allocateDirect(data.length * C.BYTES_PER_FLOAT); + bb.order(ByteOrder.nativeOrder()); + FloatBuffer buffer = bb.asFloatBuffer(); + buffer.put(data); + buffer.position(0); + + return buffer; + } + + /** + * Creates a GL_TEXTURE_EXTERNAL_OES with default configuration of GL_LINEAR filtering and + * GL_CLAMP_TO_EDGE wrapping. + */ + @TargetApi(15) + public static int createExternalTexture() { + int[] texId = new int[1]; + GLES20.glGenTextures(1, IntBuffer.wrap(texId)); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId[0]); + GLES20.glTexParameteri( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + checkGlError(); + return texId[0]; + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/Mesh.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/Mesh.java new file mode 100644 index 0000000000..d3d7d854ae --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/Mesh.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ui.spherical; + +import static com.google.android.exoplayer2.ui.spherical.GlUtil.checkGlError; + +import android.annotation.TargetApi; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import com.google.android.exoplayer2.C; +import java.nio.FloatBuffer; + +/** + * Utility class to generate & render spherical meshes for video or images. Use the static creation + * methods to construct the Mesh's data. Then call the Mesh constructor on the GL thread when ready. + * Use glDraw method to render it. + */ +@TargetApi(15) +/*package*/ final class Mesh { + + /** Defines the constants identifying the current eye type. */ + /*package*/ interface EyeType { + /** Single eye in monocular rendering. */ + int MONOCULAR = 0; + + /** The left eye in stereo rendering. */ + int LEFT = 1; + + /** The right eye in stereo rendering. */ + int RIGHT = 2; + } + + // Basic vertex & fragment shaders to render a mesh with 3D position & 2D texture data. + private static final String[] VERTEX_SHADER_CODE = + new String[] { + "uniform mat4 uMvpMatrix;", + "attribute vec4 aPosition;", + "attribute vec2 aTexCoords;", + "varying vec2 vTexCoords;", + + // Standard transformation. + "void main() {", + " gl_Position = uMvpMatrix * aPosition;", + " vTexCoords = aTexCoords;", + "}" + }; + private static final String[] FRAGMENT_SHADER_CODE = + new String[] { + // This is required since the texture data is GL_TEXTURE_EXTERNAL_OES. + "#extension GL_OES_EGL_image_external : require", + "precision mediump float;", + + // Standard texture rendering shader. + "uniform samplerExternalOES uTexture;", + "varying vec2 vTexCoords;", + "void main() {", + " gl_FragColor = texture2D(uTexture, vTexCoords);", + "}" + }; + + // Constants related to vertex data. + private static final int POSITION_COORDS_PER_VERTEX = 3; // X, Y, Z. + // The vertex contains texture coordinates for both the left & right eyes. If the scene is + // rendered in VR, the appropriate part of the vertex will be selected at runtime. For a mono + // scene, only the left eye's UV coordinates are used. + // For mono media, the UV coordinates are duplicated in each. For stereo media, the UV coords + // point to the appropriate part of the source media. + private static final int TEXTURE_COORDS_PER_VERTEX = 2 * 2; + private static final int COORDS_PER_VERTEX = + POSITION_COORDS_PER_VERTEX + TEXTURE_COORDS_PER_VERTEX; + // Data is tightly packed. Each vertex is [x, y, z, u_left, v_left, u_right, v_right]. + private static final int VERTEX_STRIDE_BYTES = COORDS_PER_VERTEX * C.BYTES_PER_FLOAT; + + // Vertices for the mesh with 3D position + left 2D texture UV + right 2D texture UV. + private final int vertixCount; + private final FloatBuffer vertexBuffer; + + // Program related GL items. These are only valid if program != 0. + private int program; + private int mvpMatrixHandle; + private int positionHandle; + private int texCoordsHandle; + private int textureHandle; + + /** + * Generates a 3D UV sphere for rendering monoscopic or stereoscopic video. + * + *

    This can be called on any thread. The returned {@link Mesh} isn't valid until {@link + * #init()} is called. + * + * @param radius Size of the sphere. Must be > 0. + * @param latitudes Number of rows that make up the sphere. Must be >= 1. + * @param longitudes Number of columns that make up the sphere. Must be >= 1. + * @param verticalFovDegrees Total latitudinal degrees that are covered by the sphere. Must be in + * (0, 180]. + * @param horizontalFovDegrees Total longitudinal degrees that are covered by the sphere.Must be + * in (0, 360]. + * @param stereoMode A {@link C.StereoMode} value. + * @return Unintialized Mesh. + */ + public static Mesh createUvSphere( + float radius, + int latitudes, + int longitudes, + float verticalFovDegrees, + float horizontalFovDegrees, + @C.StereoMode int stereoMode) { + return new Mesh( + createUvSphereVertexData( + radius, latitudes, longitudes, verticalFovDegrees, horizontalFovDegrees, stereoMode)); + } + + /** Used by static constructors. */ + private Mesh(float[] vertexData) { + vertixCount = vertexData.length / COORDS_PER_VERTEX; + vertexBuffer = GlUtil.createBuffer(vertexData); + } + + /** Initializes of the GL components. */ + /* package */ void init() { + program = GlUtil.compileProgram(VERTEX_SHADER_CODE, FRAGMENT_SHADER_CODE); + mvpMatrixHandle = GLES20.glGetUniformLocation(program, "uMvpMatrix"); + positionHandle = GLES20.glGetAttribLocation(program, "aPosition"); + texCoordsHandle = GLES20.glGetAttribLocation(program, "aTexCoords"); + textureHandle = GLES20.glGetUniformLocation(program, "uTexture"); + } + + /** + * Renders the mesh. This must be called on the GL thread. + * + * @param textureId GL_TEXTURE_EXTERNAL_OES used for this mesh. + * @param mvpMatrix The Model View Projection matrix. + * @param eyeType An {@link EyeType} value. + */ + /* package */ void draw(int textureId, float[] mvpMatrix, int eyeType) { + // Configure shader. + GLES20.glUseProgram(program); + checkGlError(); + + GLES20.glEnableVertexAttribArray(positionHandle); + GLES20.glEnableVertexAttribArray(texCoordsHandle); + checkGlError(); + + GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, mvpMatrix, 0); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId); + GLES20.glUniform1i(textureHandle, 0); + checkGlError(); + + // Load position data. + vertexBuffer.position(0); + GLES20.glVertexAttribPointer( + positionHandle, + POSITION_COORDS_PER_VERTEX, + GLES20.GL_FLOAT, + false, + VERTEX_STRIDE_BYTES, + vertexBuffer); + checkGlError(); + + // Load texture data. Eye.Type.RIGHT uses the left eye's data. + int textureOffset = + (eyeType == EyeType.RIGHT) ? POSITION_COORDS_PER_VERTEX + 2 : POSITION_COORDS_PER_VERTEX; + vertexBuffer.position(textureOffset); + GLES20.glVertexAttribPointer( + texCoordsHandle, + TEXTURE_COORDS_PER_VERTEX, + GLES20.GL_FLOAT, + false, + VERTEX_STRIDE_BYTES, + vertexBuffer); + checkGlError(); + + // Render. + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, vertixCount); + checkGlError(); + + GLES20.glDisableVertexAttribArray(positionHandle); + GLES20.glDisableVertexAttribArray(texCoordsHandle); + } + + /** Cleans up the GL resources. */ + /* package */ void shutdown() { + if (program != 0) { + GLES20.glDeleteProgram(program); + } + } + + // @VisibleForTesting + /*package*/ static float[] createUvSphereVertexData( + float radius, + int latitudes, + int longitudes, + float verticalFovDegrees, + float horizontalFovDegrees, + @C.StereoMode int stereoMode) { + if (radius <= 0 + || latitudes < 1 + || longitudes < 1 + || verticalFovDegrees <= 0 + || verticalFovDegrees > 180 + || horizontalFovDegrees <= 0 + || horizontalFovDegrees > 360) { + throw new IllegalArgumentException("Invalid parameters for sphere."); + } + + // Compute angular size in radians of each UV quad. + float verticalFovRads = (float) Math.toRadians(verticalFovDegrees); + float horizontalFovRads = (float) Math.toRadians(horizontalFovDegrees); + float quadHeightRads = verticalFovRads / latitudes; + float quadWidthRads = horizontalFovRads / longitudes; + + // Each latitude strip has 2 * (longitudes quads + extra edge) vertices + 2 degenerate vertices. + int vertexCount = (2 * (longitudes + 1) + 2) * latitudes; + // Buffer to return. + float[] vertexData = new float[vertexCount * COORDS_PER_VERTEX]; + + // Generate the data for the sphere which is a set of triangle strips representing each + // latitude band. + int offset = 0; // Offset into the vertexData array. + // (i, j) represents a quad in the equirectangular sphere. + for (int j = 0; j < latitudes; ++j) { // For each horizontal triangle strip. + // Each latitude band lies between the two phi values. Each vertical edge on a band lies on + // a theta value. + float phiLow = (quadHeightRads * j - verticalFovRads / 2); + float phiHigh = (quadHeightRads * (j + 1) - verticalFovRads / 2); + + for (int i = 0; i < longitudes + 1; ++i) { // For each vertical edge in the band. + for (int k = 0; k < 2; ++k) { // For low and high points on an edge. + // For each point, determine it's position in polar coordinates. + float phi = (k == 0) ? phiLow : phiHigh; + float theta = quadWidthRads * i + (float) Math.PI - horizontalFovRads / 2; + + // Set vertex position data as Cartesian coordinates. + vertexData[offset] = -(float) (radius * Math.sin(theta) * Math.cos(phi)); + vertexData[offset + 1] = (float) (radius * Math.sin(phi)); + vertexData[offset + 2] = (float) (radius * Math.cos(theta) * Math.cos(phi)); + + // Set vertex texture.x data. + if (stereoMode == C.STEREO_MODE_LEFT_RIGHT) { + // For left-right media, each eye's x coordinate points to the left or right half of the + // texture. + vertexData[offset + 3] = (i * quadWidthRads / horizontalFovRads) / 2; + vertexData[offset + 5] = (i * quadWidthRads / horizontalFovRads) / 2 + .5f; + } else { + // For top-bottom or monoscopic media, the eye's x spans the full width of the texture. + vertexData[offset + 3] = i * quadWidthRads / horizontalFovRads; + vertexData[offset + 5] = i * quadWidthRads / horizontalFovRads; + } + + // Set vertex texture.y data. The "1 - ..." is due to Canvas vs GL coords. + if (stereoMode == C.STEREO_MODE_TOP_BOTTOM) { + // For top-bottom media, each eye's y coordinate points to the top or bottom half of the + // texture. + vertexData[offset + 4] = 1 - (((j + k) * quadHeightRads / verticalFovRads) / 2 + .5f); + vertexData[offset + 6] = 1 - ((j + k) * quadHeightRads / verticalFovRads) / 2; + } else { + // For left-right or monoscopic media, the eye's y spans the full height of the texture. + vertexData[offset + 4] = 1 - (j + k) * quadHeightRads / verticalFovRads; + vertexData[offset + 6] = 1 - (j + k) * quadHeightRads / verticalFovRads; + } + offset += COORDS_PER_VERTEX; + + // Break up the triangle strip with degenerate vertices by copying first and last points. + if ((i == 0 && k == 0) || (i == longitudes && k == 1)) { + System.arraycopy( + vertexData, offset - COORDS_PER_VERTEX, vertexData, offset, COORDS_PER_VERTEX); + offset += COORDS_PER_VERTEX; + } + } + // Move on to the next vertical edge in the triangle strip. + } + // Move on to the next triangle strip. + } + return vertexData; + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java new file mode 100644 index 0000000000..96788000ca --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ui.spherical; + +import static com.google.android.exoplayer2.ui.spherical.GlUtil.checkGlError; + +import android.graphics.SurfaceTexture; +import android.opengl.GLES20; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.ui.spherical.Mesh.EyeType; +import com.google.android.exoplayer2.util.Assertions; +import java.util.concurrent.atomic.AtomicBoolean; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Renders a GL Scene. + * + *

    All methods should be called only on the GL thread unless GL thread is stopped. + */ +/*package*/ final class SceneRenderer { + + private final AtomicBoolean frameAvailable; + + private int textureId; + @Nullable private SurfaceTexture surfaceTexture; + @MonotonicNonNull private Mesh mesh; + private boolean meshInitialized; + + public SceneRenderer() { + frameAvailable = new AtomicBoolean(); + } + + /** Initializes the renderer. */ + public SurfaceTexture init() { + // Set the background frame color. This is only visible if the display mesh isn't a full sphere. + GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f); + checkGlError(); + + textureId = GlUtil.createExternalTexture(); + surfaceTexture = new SurfaceTexture(textureId); + surfaceTexture.setOnFrameAvailableListener(surfaceTexture -> frameAvailable.set(true)); + return surfaceTexture; + } + + /** Sets a {@link Mesh} to be used to display video. */ + public void setMesh(Mesh mesh) { + if (this.mesh != null) { + this.mesh.shutdown(); + } + this.mesh = mesh; + meshInitialized = false; + } + + /** + * Draws the scene with a given eye pose and type. + * + * @param viewProjectionMatrix 16 element GL matrix. + * @param eyeType an {@link EyeType} value + */ + public void drawFrame(float[] viewProjectionMatrix, int eyeType) { + if (mesh == null) { + return; + } + if (!meshInitialized) { + meshInitialized = true; + mesh.init(); + } + + // glClear isn't strictly necessary when rendering fully spherical panoramas, but it can improve + // performance on tiled renderers by causing the GPU to discard previous data. + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + checkGlError(); + + if (frameAvailable.compareAndSet(true, false)) { + Assertions.checkNotNull(surfaceTexture).updateTexImage(); + checkGlError(); + } + + mesh.draw(textureId, viewProjectionMatrix, eyeType); + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java new file mode 100644 index 0000000000..f4386a44c9 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ui.spherical; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.PointF; +import android.graphics.SurfaceTexture; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.opengl.Matrix; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.AnyThread; +import android.support.annotation.BinderThread; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.util.AttributeSet; +import android.view.Display; +import android.view.Surface; +import android.view.WindowManager; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ui.spherical.Mesh.EyeType; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * Renders a GL scene in a non-VR Activity that is affected by phone orientation and touch input. + * + *

    The two input components are the TYPE_GAME_ROTATION_VECTOR Sensor and a TouchListener. The GL + * renderer combines these two inputs to render a scene with the appropriate camera orientation. + * + *

    The primary complexity in this class is related to the various rotations. It is important to + * apply the touch and sensor rotations in the correct order or the user's touch manipulations won't + * match what they expect. + */ +@TargetApi(15) +public final class SphericalSurfaceView extends GLSurfaceView { + + /** + * This listener can be used to be notified when the {@link Surface} associated with this view is + * changed. + */ + public interface SurfaceListener { + /** + * Invoked when the surface is changed or there isn't one anymore. Any previous surface + * shouldn't be used after this call. + * + * @param surface The new surface or null if there isn't one anymore. + */ + void surfaceChanged(@Nullable Surface surface); + } + + // A spherical mesh for video should be large enough that there are no stereo artifacts. + private static final int SPHERE_RADIUS_METERS = 50; + + // TODO These should be configured based on the video type. It's assumed 360 video here. + private static final int DEFAULT_SPHERE_HORIZONTAL_DEGREES = 360; + private static final int DEFAULT_SPHERE_VERTICAL_DEGREES = 180; + + // The 360 x 180 sphere has 5 degree quads. Increase these if lines in videos look wavy. + private static final int DEFAULT_SPHERE_COLUMNS = 72; + private static final int DEFAULT_SPHERE_ROWS = 36; + + // Arbitrary vertical field of view. + private static final int FIELD_OF_VIEW_DEGREES = 90; + private static final float Z_NEAR = .1f; + private static final float Z_FAR = 100; + + // TODO Calculate this depending on surface size and field of view. + private static final float PX_PER_DEGREES = 25; + + /*package*/ static final float UPRIGHT_ROLL = (float) Math.PI; + + private final SensorManager sensorManager; + private final @Nullable Sensor orientationSensor; + private final PhoneOrientationListener phoneOrientationListener; + private final Renderer renderer; + private final Handler mainHandler; + private @Nullable SurfaceListener surfaceListener; + private @Nullable SurfaceTexture surfaceTexture; + private @Nullable Surface surface; + + public SphericalSurfaceView(Context context) { + this(context, null); + } + + public SphericalSurfaceView(Context context, @Nullable AttributeSet attributeSet) { + super(context, attributeSet); + + mainHandler = new Handler(Looper.getMainLooper()); + + // Configure sensors and touch. + sensorManager = + (SensorManager) Assertions.checkNotNull(context.getSystemService(Context.SENSOR_SERVICE)); + // TYPE_GAME_ROTATION_VECTOR is the easiest sensor since it handles all the complex math for + // fusion. It's used instead of TYPE_ROTATION_VECTOR since the latter uses the magnetometer on + // devices. When used indoors, the magnetometer can take some time to settle depending on the + // device and amount of metal in the environment. + int type = Util.SDK_INT >= 18 ? Sensor.TYPE_GAME_ROTATION_VECTOR : Sensor.TYPE_ROTATION_VECTOR; + orientationSensor = sensorManager.getDefaultSensor(type); + + renderer = new Renderer(); + + TouchTracker touchTracker = new TouchTracker(renderer, PX_PER_DEGREES); + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = Assertions.checkNotNull(windowManager).getDefaultDisplay(); + phoneOrientationListener = new PhoneOrientationListener(display, touchTracker, renderer); + + setEGLContextClientVersion(2); + setRenderer(renderer); + setOnTouchListener(touchTracker); + + setStereoMode(C.STEREO_MODE_MONO); + } + + /** + * Sets stereo mode of the media to be played. + * + * @param stereoMode One of {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link + * C#STEREO_MODE_LEFT_RIGHT}. + */ + public void setStereoMode(@C.StereoMode int stereoMode) { + Assertions.checkState( + stereoMode == C.STEREO_MODE_MONO + || stereoMode == C.STEREO_MODE_TOP_BOTTOM + || stereoMode == C.STEREO_MODE_LEFT_RIGHT); + Mesh mesh = + Mesh.createUvSphere( + SPHERE_RADIUS_METERS, + DEFAULT_SPHERE_ROWS, + DEFAULT_SPHERE_COLUMNS, + DEFAULT_SPHERE_VERTICAL_DEGREES, + DEFAULT_SPHERE_HORIZONTAL_DEGREES, + stereoMode); + queueEvent(() -> renderer.scene.setMesh(mesh)); + } + + /** Returns the {@link Surface} associated with this view. */ + public @Nullable Surface getSurface() { + return surface; + } + + /** + * Sets the {@link SurfaceListener} used to listen to surface events. + * + * @param listener The listener for surface events. + */ + public void setSurfaceListener(@Nullable SurfaceListener listener) { + surfaceListener = listener; + } + + @Override + public void onResume() { + super.onResume(); + if (orientationSensor != null) { + sensorManager.registerListener( + phoneOrientationListener, orientationSensor, SensorManager.SENSOR_DELAY_FASTEST); + } + } + + @Override + public void onPause() { + if (orientationSensor != null) { + sensorManager.unregisterListener(phoneOrientationListener); + } + super.onPause(); + } + + @Override + protected void onDetachedFromWindow() { + // This call stops GL thread. + super.onDetachedFromWindow(); + + // Post to make sure we occur in order with any onSurfaceTextureAvailable calls. + mainHandler.post( + () -> { + if (surface != null) { + if (surfaceListener != null) { + surfaceListener.surfaceChanged(null); + } + releaseSurface(surfaceTexture, surface); + surfaceTexture = null; + surface = null; + } + }); + } + + // Called on GL thread. + private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) { + mainHandler.post( + () -> { + SurfaceTexture oldSurfaceTexture = this.surfaceTexture; + Surface oldSurface = this.surface; + this.surfaceTexture = surfaceTexture; + this.surface = new Surface(surfaceTexture); + if (surfaceListener != null) { + surfaceListener.surfaceChanged(surface); + } + releaseSurface(oldSurfaceTexture, oldSurface); + }); + } + + private static void releaseSurface( + @Nullable SurfaceTexture oldSurfaceTexture, @Nullable Surface oldSurface) { + if (oldSurfaceTexture != null) { + oldSurfaceTexture.release(); + } + if (oldSurface != null) { + oldSurface.release(); + } + } + + /** Detects sensor events and saves them as a matrix. */ + private static class PhoneOrientationListener implements SensorEventListener { + private final float[] phoneInWorldSpaceMatrix = new float[16]; + private final float[] remappedPhoneMatrix = new float[16]; + private final float[] angles = new float[3]; + private final Display display; + private final TouchTracker touchTracker; + private final Renderer renderer; + + public PhoneOrientationListener(Display display, TouchTracker touchTracker, Renderer renderer) { + this.display = display; + this.touchTracker = touchTracker; + this.renderer = renderer; + } + + @Override + @BinderThread + public void onSensorChanged(SensorEvent event) { + SensorManager.getRotationMatrixFromVector(remappedPhoneMatrix, event.values); + + // If we're not in upright portrait mode, remap the axes of the coordinate system according to + // the display rotation. + int xAxis; + int yAxis; + switch (display.getRotation()) { + case Surface.ROTATION_270: + xAxis = SensorManager.AXIS_MINUS_Y; + yAxis = SensorManager.AXIS_X; + break; + case Surface.ROTATION_180: + xAxis = SensorManager.AXIS_MINUS_X; + yAxis = SensorManager.AXIS_MINUS_Y; + break; + case Surface.ROTATION_90: + xAxis = SensorManager.AXIS_Y; + yAxis = SensorManager.AXIS_MINUS_X; + break; + case Surface.ROTATION_0: + default: + xAxis = SensorManager.AXIS_X; + yAxis = SensorManager.AXIS_Y; + break; + } + SensorManager.remapCoordinateSystem( + remappedPhoneMatrix, xAxis, yAxis, phoneInWorldSpaceMatrix); + + // Extract the phone's roll and pass it on to touchTracker & renderer. Remapping is required + // since we need the calculated roll of the phone to be independent of the phone's pitch & + // yaw. Any operation that decomposes rotation to Euler angles needs to be performed + // carefully. + SensorManager.remapCoordinateSystem( + phoneInWorldSpaceMatrix, + SensorManager.AXIS_X, + SensorManager.AXIS_MINUS_Z, + remappedPhoneMatrix); + SensorManager.getOrientation(remappedPhoneMatrix, angles); + float roll = angles[2]; + touchTracker.setRoll(roll); + + // Rotate from Android coordinates to OpenGL coordinates. Android's coordinate system + // assumes Y points North and Z points to the sky. OpenGL has Y pointing up and Z pointing + // toward the user. + Matrix.rotateM(phoneInWorldSpaceMatrix, 0, 90, 1, 0, 0); + renderer.setDeviceOrientation(phoneInWorldSpaceMatrix, roll); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) {} + } + + /** + * Standard GL Renderer implementation. The notable code is the matrix multiplication in + * onDrawFrame and updatePitchMatrix. + */ + // @VisibleForTesting + /*package*/ class Renderer implements GLSurfaceView.Renderer, TouchTracker.Listener { + private final SceneRenderer scene; + private final float[] projectionMatrix = new float[16]; + + // There is no model matrix for this scene so viewProjectionMatrix is used for the mvpMatrix. + private final float[] viewProjectionMatrix = new float[16]; + + // Device orientation is derived from sensor data. This is accessed in the sensor's thread and + // the GL thread. + private final float[] deviceOrientationMatrix = new float[16]; + + // Optional pitch and yaw rotations are applied to the sensor orientation. These are accessed on + // the UI, sensor and GL Threads. + private final float[] touchPitchMatrix = new float[16]; + private final float[] touchYawMatrix = new float[16]; + private float touchPitch; + private float deviceRoll; + + // viewMatrix = touchPitch * deviceOrientation * touchYaw. + private final float[] viewMatrix = new float[16]; + private final float[] tempMatrix = new float[16]; + + public Renderer() { + scene = new SceneRenderer(); + Matrix.setIdentityM(deviceOrientationMatrix, 0); + Matrix.setIdentityM(touchPitchMatrix, 0); + Matrix.setIdentityM(touchYawMatrix, 0); + deviceRoll = UPRIGHT_ROLL; + } + + @Override + public synchronized void onSurfaceCreated(GL10 gl, EGLConfig config) { + onSurfaceTextureAvailable(scene.init()); + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + GLES20.glViewport(0, 0, width, height); + float aspect = (float) width / height; + float fovY = calculateFieldOfViewInYDirection(aspect); + Matrix.perspectiveM(projectionMatrix, 0, fovY, aspect, Z_NEAR, Z_FAR); + } + + @Override + public void onDrawFrame(GL10 gl) { + // Combine touch & sensor data. + // Orientation = pitch * sensor * yaw since that is closest to what most users expect the + // behavior to be. + synchronized (this) { + Matrix.multiplyMM(tempMatrix, 0, deviceOrientationMatrix, 0, touchYawMatrix, 0); + Matrix.multiplyMM(viewMatrix, 0, touchPitchMatrix, 0, tempMatrix, 0); + } + + Matrix.multiplyMM(viewProjectionMatrix, 0, projectionMatrix, 0, viewMatrix, 0); + scene.drawFrame(viewProjectionMatrix, EyeType.MONOCULAR); + } + + /** Adjusts the GL camera's rotation based on device rotation. Runs on the sensor thread. */ + @BinderThread + public synchronized void setDeviceOrientation(float[] matrix, float deviceRoll) { + System.arraycopy(matrix, 0, deviceOrientationMatrix, 0, deviceOrientationMatrix.length); + this.deviceRoll = -deviceRoll; + updatePitchMatrix(); + } + + /** + * Updates the pitch matrix after a physical rotation or touch input. The pitch matrix rotation + * is applied on an axis that is dependent on device rotation so this must be called after + * either touch or sensor update. + */ + @AnyThread + private void updatePitchMatrix() { + // The camera's pitch needs to be rotated along an axis that is parallel to the real world's + // horizon. This is the <1, 0, 0> axis after compensating for the device's roll. + Matrix.setRotateM( + touchPitchMatrix, + 0, + -touchPitch, + (float) Math.cos(deviceRoll), + (float) Math.sin(deviceRoll), + 0); + } + + @Override + @UiThread + public synchronized void onScrollChange(PointF scrollOffsetDegrees) { + touchPitch = scrollOffsetDegrees.y; + updatePitchMatrix(); + Matrix.setRotateM(touchYawMatrix, 0, -scrollOffsetDegrees.x, 0, 1, 0); + } + + private float calculateFieldOfViewInYDirection(float aspect) { + boolean landscapeMode = aspect > 1; + if (landscapeMode) { + double halfFovX = FIELD_OF_VIEW_DEGREES / 2; + double tanY = Math.tan(Math.toRadians(halfFovX)) / aspect; + double halfFovY = Math.toDegrees(Math.atan(tanY)); + return (float) (halfFovY * 2); + } else { + return FIELD_OF_VIEW_DEGREES; + } + } + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/TouchTracker.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/TouchTracker.java new file mode 100644 index 0000000000..ea3a0b4e16 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/TouchTracker.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ui.spherical; + +import android.graphics.PointF; +import android.support.annotation.BinderThread; +import android.view.MotionEvent; +import android.view.View; + +/** + * Basic touch input system. + * + *

    Mixing touch input and gyro input results in a complicated UI so this should be used + * carefully. This touch system implements a basic (X, Y) -> (yaw, pitch) transform. This works for + * basic UI but fails in edge cases where the user tries to drag scene up or down. There is no good + * UX solution for this. The least bad solution is to disable pitch manipulation and only let the + * user adjust yaw. This example tries to limit the awkwardness by restricting pitch manipulation to + * +/- 45 degrees. + * + *

    It is also important to get the order of operations correct. To match what users expect, touch + * interaction manipulates the scene by rotating the world by the yaw offset and tilting the camera + * by the pitch offset. If the order of operations is incorrect, the sensors & touch rotations will + * have strange interactions. The roll of the phone is also tracked so that the x & y are correctly + * mapped to yaw & pitch no matter how the user holds their phone. + * + *

    This class doesn't handle any scrolling inertia but Android's + * com.google.vr.sdk.widgets.common.TouchTracker.FlingGestureListener can be used with this code for + * a nicer UI. An even more advanced UI would reproject the user's touch point into 3D and drag the + * Mesh as the user moves their finger. However, that requires quaternion interpolation. + */ +// @VisibleForTesting +/*package*/ class TouchTracker implements View.OnTouchListener { + + /*package*/ interface Listener { + void onScrollChange(PointF scrollOffsetDegrees); + } + + // Touch input won't change the pitch beyond +/- 45 degrees. This reduces awkward situations + // where the touch-based pitch and gyro-based pitch interact badly near the poles. + /*package*/ static final float MAX_PITCH_DEGREES = 45; + + // With every touch event, update the accumulated degrees offset by the new pixel amount. + private final PointF previousTouchPointPx = new PointF(); + private final PointF accumulatedTouchOffsetDegrees = new PointF(); + + private final Listener listener; + private final float pxPerDegrees; + // The conversion from touch to yaw & pitch requires compensating for device roll. This is set + // on the sensor thread and read on the UI thread. + private volatile float roll; + + public TouchTracker(Listener listener, float pxPerDegrees) { + this.listener = listener; + this.pxPerDegrees = pxPerDegrees; + roll = SphericalSurfaceView.UPRIGHT_ROLL; + } + + /** + * Converts ACTION_MOVE events to pitch & yaw events while compensating for device roll. + * + * @return true if we handled the event + */ + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + // Initialize drag gesture. + previousTouchPointPx.set(event.getX(), event.getY()); + return true; + case MotionEvent.ACTION_MOVE: + // Calculate the touch delta in screen space. + float touchX = (event.getX() - previousTouchPointPx.x) / pxPerDegrees; + float touchY = (event.getY() - previousTouchPointPx.y) / pxPerDegrees; + previousTouchPointPx.set(event.getX(), event.getY()); + + float r = roll; // Copy volatile state. + float cr = (float) Math.cos(r); + float sr = (float) Math.sin(r); + // To convert from screen space to the 3D space, we need to adjust the drag vector based + // on the roll of the phone. This is standard rotationMatrix(roll) * vector math but has + // an inverted y-axis due to the screen-space coordinates vs GL coordinates. + // Handle yaw. + accumulatedTouchOffsetDegrees.x -= cr * touchX - sr * touchY; + // Handle pitch and limit it to 45 degrees. + accumulatedTouchOffsetDegrees.y += sr * touchX + cr * touchY; + accumulatedTouchOffsetDegrees.y = + Math.max( + -MAX_PITCH_DEGREES, Math.min(MAX_PITCH_DEGREES, accumulatedTouchOffsetDegrees.y)); + + listener.onScrollChange(accumulatedTouchOffsetDegrees); + return true; + default: + return false; + } + } + + @BinderThread + public void setRoll(float roll) { + // We compensate for roll by rotating in the opposite direction. + this.roll = -roll; + } +} diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_fullscreen_enter.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_fullscreen_enter.png index 9b8131124d..1af1f68a02 100644 Binary files a/library/ui/src/main/res/drawable-ldpi/exo_controls_fullscreen_enter.png and b/library/ui/src/main/res/drawable-ldpi/exo_controls_fullscreen_enter.png differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_fullscreen_exit.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_fullscreen_exit.png index 159bea7fd8..db03f46435 100644 Binary files a/library/ui/src/main/res/drawable-ldpi/exo_controls_fullscreen_exit.png and b/library/ui/src/main/res/drawable-ldpi/exo_controls_fullscreen_exit.png differ diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index e127f181e9..89f873edf7 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -29,6 +29,7 @@ + @@ -50,7 +51,11 @@ - + + + + + diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index d5d524b5a5..f40d30f331 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java index 5267d54bef..79d39096c5 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java @@ -20,11 +20,11 @@ import static com.google.common.truth.Truth.assertWithMessage; import android.net.Uri; import android.test.ActivityInstrumentationTestCase2; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.dash.DashUtil; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.Representation; -import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.source.dash.offline.DashDownloader; import com.google.android.exoplayer2.testutil.HostActivity; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; @@ -106,7 +106,7 @@ public final class DashDownloadTest extends ActivityInstrumentationTestCase2 keys = new ArrayList<>(); + ArrayList keys = new ArrayList<>(); for (int pIndex = 0; pIndex < dashManifest.getPeriodCount(); pIndex++) { List adaptationSets = dashManifest.getPeriod(pIndex).adaptationSets; for (int aIndex = 0; aIndex < adaptationSets.size(); aIndex++) { @@ -116,7 +116,7 @@ public final class DashDownloadTest extends ActivityInstrumentationTestCase2 drmSessionManager) { SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance( - new DebugRenderersFactory(host), trackSelector, drmSessionManager); + host, new DebugRenderersFactory(host), trackSelector, drmSessionManager); player.setVideoSurface(surface); return player; } @Override - protected MediaSource buildSource(HostActivity host, String userAgent, - TransferListener mediaTransferListener) { + protected MediaSource buildSource( + HostActivity host, String userAgent, TransferListener mediaTransferListener) { DataSource.Factory manifestDataSourceFactory = dataSourceFactory != null ? dataSourceFactory : new DefaultDataSourceFactory(host, userAgent); DataSource.Factory mediaDataSourceFactory = dataSourceFactory != null diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java new file mode 100644 index 0000000000..5157ab672c --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.playbacktests.gts; + +import android.media.MediaCodecInfo.AudioCapabilities; +import android.media.MediaCodecInfo.CodecCapabilities; +import android.media.MediaCodecInfo.CodecProfileLevel; +import android.media.MediaCodecInfo.VideoCapabilities; +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; +import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; +import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import com.google.android.exoplayer2.testutil.MetricsLogger; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; +import java.util.List; + +/** Tests enumeration of decoders using {@link MediaCodecUtil}. */ +public class EnumerateDecodersTest extends InstrumentationTestCase { + + private static final String TAG = "EnumerateDecodersTest"; + + private static final String REPORT_NAME = "GtsExoPlayerTestCases"; + private static final String REPORT_OBJECT_NAME = "enumeratedecoderstest"; + + private MetricsLogger metricsLogger; + + @Override + protected void setUp() throws Exception { + super.setUp(); + metricsLogger = + MetricsLogger.Factory.createDefault( + getInstrumentation(), TAG, REPORT_NAME, REPORT_OBJECT_NAME); + } + + public void testEnumerateDecoders() throws Exception { + enumerateDecoders(MimeTypes.VIDEO_H263); + enumerateDecoders(MimeTypes.VIDEO_H264); + enumerateDecoders(MimeTypes.VIDEO_H265); + enumerateDecoders(MimeTypes.VIDEO_VP8); + enumerateDecoders(MimeTypes.VIDEO_VP9); + enumerateDecoders(MimeTypes.VIDEO_MP4V); + enumerateDecoders(MimeTypes.VIDEO_MPEG); + enumerateDecoders(MimeTypes.VIDEO_MPEG2); + enumerateDecoders(MimeTypes.VIDEO_VC1); + enumerateDecoders(MimeTypes.AUDIO_AAC); + enumerateDecoders(MimeTypes.AUDIO_MPEG_L1); + enumerateDecoders(MimeTypes.AUDIO_MPEG_L2); + enumerateDecoders(MimeTypes.AUDIO_MPEG); + enumerateDecoders(MimeTypes.AUDIO_RAW); + enumerateDecoders(MimeTypes.AUDIO_ALAW); + enumerateDecoders(MimeTypes.AUDIO_MLAW); + enumerateDecoders(MimeTypes.AUDIO_AC3); + enumerateDecoders(MimeTypes.AUDIO_E_AC3); + enumerateDecoders(MimeTypes.AUDIO_E_AC3_JOC); + enumerateDecoders(MimeTypes.AUDIO_TRUEHD); + enumerateDecoders(MimeTypes.AUDIO_DTS); + enumerateDecoders(MimeTypes.AUDIO_DTS_HD); + enumerateDecoders(MimeTypes.AUDIO_DTS_EXPRESS); + enumerateDecoders(MimeTypes.AUDIO_VORBIS); + enumerateDecoders(MimeTypes.AUDIO_OPUS); + enumerateDecoders(MimeTypes.AUDIO_AMR_NB); + enumerateDecoders(MimeTypes.AUDIO_AMR_WB); + enumerateDecoders(MimeTypes.AUDIO_FLAC); + enumerateDecoders(MimeTypes.AUDIO_ALAC); + enumerateDecoders(MimeTypes.AUDIO_MSGSM); + } + + private void enumerateDecoders(String mimeType) throws DecoderQueryException { + logDecoderInfos(MediaCodecUtil.getDecoderInfos(mimeType, /* secure= */ false)); + logDecoderInfos(MediaCodecUtil.getDecoderInfos(mimeType, /* secure= */ true)); + } + + private void logDecoderInfos(List mediaCodecInfos) { + for (MediaCodecInfo mediaCodecInfo : mediaCodecInfos) { + CodecCapabilities capabilities = Assertions.checkNotNull(mediaCodecInfo.capabilities); + metricsLogger.logMetric( + "capabilities_" + mediaCodecInfo.name, codecCapabilitiesToString(capabilities)); + } + } + + private static String codecCapabilitiesToString(CodecCapabilities codecCapabilities) { + String mimeType = codecCapabilities.getMimeType(); + boolean isVideo = MimeTypes.isVideo(mimeType); + boolean isAudio = MimeTypes.isAudio(mimeType); + StringBuilder result = new StringBuilder(); + result.append("[mimeType=").append(mimeType).append(", profileLevels="); + profileLevelsToString(codecCapabilities.profileLevels, result); + result.append(", maxSupportedInstances=").append(codecCapabilities.getMaxSupportedInstances()); + if (isVideo) { + result.append(", videoCapabilities="); + videoCapabilitiesToString(codecCapabilities.getVideoCapabilities(), result); + result.append(", colorFormats=").append(Arrays.toString(codecCapabilities.colorFormats)); + } else if (isAudio) { + result.append(", audioCapabilities="); + audioCapabilitiesToString(codecCapabilities.getAudioCapabilities(), result); + } + if (Util.SDK_INT >= 19 + && isVideo + && codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback)) { + result.append(", FEATURE_AdaptivePlayback"); + } + if (Util.SDK_INT >= 21 + && isVideo + && codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback)) { + result.append(", FEATURE_SecurePlayback"); + } + if (Util.SDK_INT >= 26 + && isVideo + && codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_PartialFrame)) { + result.append(", FEATURE_PartialFrame"); + } + if (Util.SDK_INT >= 21 + && (isVideo || isAudio) + && codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback)) { + result.append(", FEATURE_TunneledPlayback"); + } + result.append(']'); + return result.toString(); + } + + private static void audioCapabilitiesToString( + AudioCapabilities audioCapabilities, StringBuilder result) { + result + .append("[bitrateRange=") + .append(audioCapabilities.getBitrateRange()) + .append(", maxInputChannelCount=") + .append(audioCapabilities.getMaxInputChannelCount()) + .append(", supportedSampleRateRanges=") + .append(Arrays.toString(audioCapabilities.getSupportedSampleRateRanges())) + .append(']'); + } + + private static void videoCapabilitiesToString( + VideoCapabilities videoCapabilities, StringBuilder result) { + result + .append("[bitrateRange=") + .append(videoCapabilities.getBitrateRange()) + .append(", heightAlignment=") + .append(videoCapabilities.getHeightAlignment()) + .append(", widthAlignment=") + .append(videoCapabilities.getWidthAlignment()) + .append(", supportedWidths=") + .append(videoCapabilities.getSupportedWidths()) + .append(", supportedHeights=") + .append(videoCapabilities.getSupportedHeights()) + .append(", supportedFrameRates=") + .append(videoCapabilities.getSupportedFrameRates()) + .append(']'); + } + + private static void profileLevelsToString( + CodecProfileLevel[] profileLevels, StringBuilder result) { + result.append('['); + int count = profileLevels.length; + for (int i = 0; i < count; i++) { + CodecProfileLevel profileLevel = profileLevels[i]; + if (i != 0) { + result.append(", "); + } + result + .append("[profile=") + .append(profileLevel.profile) + .append(", level=") + .append(profileLevel.level) + .append(']'); + } + result.append(']'); + } +} diff --git a/testutils/build.gradle b/testutils/build.gradle index a7f05a2c5e..2ef377ba5d 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion @@ -36,5 +41,7 @@ dependencies { api 'com.google.truth:truth:' + truthVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation project(modulePrefix + 'library-core') + implementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion + annotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index a6c3438a52..a6f672e54d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -572,10 +572,12 @@ public abstract class Action { return; } Player.EventListener listener = - new Player.DefaultEventListener() { + new Player.EventListener() { @Override public void onTimelineChanged( - Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { + Timeline timeline, + @Nullable Object manifest, + @Player.TimelineChangeReason int reason) { if (expectedTimeline == null || timeline.equals(expectedTimeline)) { player.removeListener(this); nextAction.schedule(player, trackSelector, surface, handler); @@ -618,13 +620,14 @@ public abstract class Action { if (nextAction == null) { return; } - player.addListener(new Player.DefaultEventListener() { - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - player.removeListener(this); - nextAction.schedule(player, trackSelector, surface, handler); - } - }); + player.addListener( + new Player.EventListener() { + @Override + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + player.removeListener(this); + nextAction.schedule(player, trackSelector, surface, handler); + } + }); } @Override @@ -663,15 +666,16 @@ public abstract class Action { if (targetPlaybackState == player.getPlaybackState()) { nextAction.schedule(player, trackSelector, surface, handler); } else { - player.addListener(new Player.DefaultEventListener() { - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (targetPlaybackState == playbackState) { - player.removeListener(this); - nextAction.schedule(player, trackSelector, surface, handler); - } - } - }); + player.addListener( + new Player.EventListener() { + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (targetPlaybackState == playbackState) { + player.removeListener(this); + nextAction.schedule(player, trackSelector, surface, handler); + } + } + }); } } @@ -704,13 +708,14 @@ public abstract class Action { if (nextAction == null) { return; } - player.addListener(new Player.DefaultEventListener() { - @Override - public void onSeekProcessed() { - player.removeListener(this); - nextAction.schedule(player, trackSelector, surface, handler); - } - }); + player.addListener( + new Player.EventListener() { + @Override + public void onSeekProcessed() { + player.removeListener(this); + nextAction.schedule(player, trackSelector, surface, handler); + } + }); } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index 4bbfef6bb8..02a8a0597d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -81,15 +81,20 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { } @Override - protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, - MediaCrypto crypto) throws DecoderQueryException { + protected void configureCodec( + MediaCodecInfo codecInfo, + MediaCodec codec, + Format format, + MediaCrypto crypto, + float operatingRate) + throws DecoderQueryException { // If the codec is being initialized whilst the renderer is started, default behavior is to // render the first frame (i.e. the keyframe before the current position), then drop frames up // to the current playback position. For test runs that place a maximum limit on the number of // dropped frames allowed, this is not desired behavior. Hence we skip (rather than drop) // frames up to the current playback position [Internal: b/66494991]. skipToPositionBeforeRenderingFirstFrame = getState() == Renderer.STATE_STARTED; - super.configureCodec(codecInfo, codec, format, crypto); + super.configureCodec(codecInfo, codec, format, crypto, operatingRate); } @Override diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index 5c8e87d38f..00e2943086 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -41,7 +41,6 @@ import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Clock; @@ -49,11 +48,12 @@ import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; -/** - * A {@link HostedTest} for {@link ExoPlayer} playback tests. - */ -public abstract class ExoHostedTest extends Player.DefaultEventListener implements HostedTest, - AudioRendererEventListener, VideoRendererEventListener { +/** A {@link HostedTest} for {@link ExoPlayer} playback tests. */ +public abstract class ExoHostedTest + implements Player.EventListener, + HostedTest, + AudioRendererEventListener, + VideoRendererEventListener { static { // DefaultAudioSink is able to work around spurious timestamps reported by the platform (by @@ -370,14 +370,15 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen DrmSessionManager drmSessionManager) { RenderersFactory renderersFactory = new DefaultRenderersFactory(host, drmSessionManager, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF, 0); - SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); + SimpleExoPlayer player = + ExoPlayerFactory.newSimpleInstance(host, renderersFactory, trackSelector); player.setVideoSurface(surface); return player; } @SuppressWarnings("unused") - protected abstract MediaSource buildSource(HostActivity host, String userAgent, - TransferListener mediaTransferListener); + protected abstract MediaSource buildSource( + HostActivity host, String userAgent, TransferListener mediaTransferListener); @SuppressWarnings("unused") protected void onPlayerErrorInternal(ExoPlaybackException error) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index cf7470b80a..5ac071d9a2 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -17,7 +17,9 @@ package com.google.android.exoplayer2.testutil; import static com.google.common.truth.Truth.assertThat; +import android.content.Context; import android.os.HandlerThread; +import android.os.Looper; import android.support.annotation.Nullable; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.ExoPlaybackException; @@ -40,6 +42,7 @@ import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.MimeTypes; @@ -50,11 +53,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -/** - * Helper class to run an ExoPlayer test. - */ -public final class ExoPlayerTestRunner extends Player.DefaultEventListener - implements ActionSchedule.Callback { +/** Helper class to run an ExoPlayer test. */ +public final class ExoPlayerTestRunner implements Player.EventListener, ActionSchedule.Callback { /** * Builder to set-up a {@link ExoPlayerTestRunner}. Default fake implementations will be used for @@ -292,9 +292,10 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener /** * Builds an {@link ExoPlayerTestRunner} using the provided values or their defaults. * + * @param context The context. * @return The built {@link ExoPlayerTestRunner}. */ - public ExoPlayerTestRunner build() { + public ExoPlayerTestRunner build(Context context) { if (supportedFormats == null) { supportedFormats = new Format[] {VIDEO_FORMAT}; } @@ -335,6 +336,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener expectedPlayerEndedCount = 1; } return new ExoPlayerTestRunner( + context, clock, mediaSource, renderersFactory, @@ -349,6 +351,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener } } + private final Context context; private final Clock clock; private final MediaSource mediaSource; private final RenderersFactory renderersFactory; @@ -376,6 +379,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private boolean playerWasPrepared; private ExoPlayerTestRunner( + Context context, Clock clock, MediaSource mediaSource, RenderersFactory renderersFactory, @@ -387,6 +391,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener @Nullable AudioRendererEventListener audioRendererEventListener, @Nullable AnalyticsListener analyticsListener, int expectedPlayerEndedCount) { + this.context = context; this.clock = clock; this.mediaSource = mediaSource; this.renderersFactory = renderersFactory; @@ -424,7 +429,9 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener @Override public void run() { try { - player = new TestSimpleExoPlayer(renderersFactory, trackSelector, loadControl, clock); + player = + new TestSimpleExoPlayer( + context, renderersFactory, trackSelector, loadControl, clock); player.addListener(ExoPlayerTestRunner.this); if (eventListener != null) { player.addListener(eventListener); @@ -601,8 +608,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener // Player.EventListener @Override - public void onTimelineChanged(Timeline timeline, Object manifest, - @Player.TimelineChangeReason int reason) { + public void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { timelines.add(timeline); manifests.add(manifest); timelineChangeReasons.add(reason); @@ -653,17 +660,21 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private static final class TestSimpleExoPlayer extends SimpleExoPlayer { public TestSimpleExoPlayer( + Context context, RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, Clock clock) { super( + context, renderersFactory, trackSelector, loadControl, /* drmSessionManager= */ null, + new DefaultBandwidthMeter.Builder().build(), new AnalyticsCollector.Factory(), - clock); + clock, + Looper.myLooper()); } } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java index 82c14a5b32..0fef8db78e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java @@ -15,9 +15,14 @@ */ package com.google.android.exoplayer2.testutil; +import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator; +import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; +import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment; +import com.google.android.exoplayer2.upstream.DataSpec; import java.util.Random; /** @@ -63,6 +68,49 @@ public final class FakeAdaptiveDataSet extends FakeDataSet { } + /** {@link MediaChunkIterator} for the chunks defined by a fake adaptive data set. */ + public static final class Iterator extends BaseMediaChunkIterator { + + private final FakeAdaptiveDataSet dataSet; + private final int trackGroupIndex; + + /** + * Create iterator. + * + * @param dataSet The data set to iterate over. + * @param trackGroupIndex The index of the track group to iterate over. + * @param chunkIndex The chunk index to which the iterator points initially. + */ + public Iterator(FakeAdaptiveDataSet dataSet, int trackGroupIndex, int chunkIndex) { + super(/* fromIndex= */ chunkIndex, /* toIndex= */ dataSet.getChunkCount() - 1); + this.dataSet = dataSet; + this.trackGroupIndex = trackGroupIndex; + } + + @Override + public DataSpec getDataSpec() { + checkInBounds(); + String uri = dataSet.getUri(trackGroupIndex); + int chunkIndex = (int) getCurrentIndex(); + Segment fakeDataChunk = dataSet.getData(uri).getSegments().get(chunkIndex); + return new DataSpec( + Uri.parse(uri), fakeDataChunk.byteOffset, fakeDataChunk.length, /* key= */ null); + } + + @Override + public long getChunkStartTimeUs() { + checkInBounds(); + return dataSet.getStartTime((int) getCurrentIndex()); + } + + @Override + public long getChunkEndTimeUs() { + checkInBounds(); + int chunkIndex = (int) getCurrentIndex(); + return dataSet.getStartTime(chunkIndex) + dataSet.getChunkDuration(chunkIndex); + } + } + private final int chunkCount; private final long chunkDurationUs; private final long lastChunkDurationUs; @@ -124,5 +172,4 @@ public final class FakeAdaptiveDataSet extends FakeDataSet { public int getChunkIndexByPosition(long positionUs) { return (int) (positionUs / chunkDurationUs); } - } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index 1008c0d561..6218e4624d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.testutil; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -24,6 +25,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.List; @@ -37,6 +39,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod private final Allocator allocator; private final FakeChunkSource.Factory chunkSourceFactory; + private final @Nullable TransferListener transferListener; private final long durationUs; private Callback callback; @@ -48,10 +51,12 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod EventDispatcher eventDispatcher, Allocator allocator, FakeChunkSource.Factory chunkSourceFactory, - long durationUs) { + long durationUs, + @Nullable TransferListener transferListener) { super(trackGroupArray, eventDispatcher); this.allocator = allocator; this.chunkSourceFactory = chunkSourceFactory; + this.transferListener = transferListener; this.durationUs = durationUs; this.sampleStreams = newSampleStreamArray(0); } @@ -128,7 +133,8 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod @Override protected SampleStream createSampleStream(TrackSelection trackSelection) { - FakeChunkSource chunkSource = chunkSourceFactory.createChunkSource(trackSelection, durationUs); + FakeChunkSource chunkSource = + chunkSourceFactory.createChunkSource(trackSelection, durationUs, transferListener); return new ChunkSampleStream<>( MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType), null, diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java index 41488b2a3b..6510c8b425 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.source.MediaSource; @@ -23,6 +24,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; /** * Fake {@link MediaSource} that provides a given timeline. Creating the period returns a @@ -49,10 +51,16 @@ public class FakeAdaptiveMediaSource extends FakeMediaSource { MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator, - EventDispatcher eventDispatcher) { + EventDispatcher eventDispatcher, + @Nullable TransferListener transferListener) { Period period = timeline.getPeriod(id.periodIndex, new Period()); return new FakeAdaptiveMediaPeriod( - trackGroupArray, eventDispatcher, allocator, chunkSourceFactory, period.durationUs); + trackGroupArray, + eventDispatcher, + allocator, + chunkSourceFactory, + period.durationUs, + transferListener); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java index 9234287e92..019d3696a7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.testutil; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; @@ -28,6 +29,7 @@ import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -52,11 +54,17 @@ public final class FakeChunkSource implements ChunkSource { this.dataSourceFactory = dataSourceFactory; } - public FakeChunkSource createChunkSource(TrackSelection trackSelection, long durationUs) { + public FakeChunkSource createChunkSource( + TrackSelection trackSelection, + long durationUs, + @Nullable TransferListener transferListener) { FakeAdaptiveDataSet dataSet = dataSetFactory.createDataSet(trackSelection.getTrackGroup(), durationUs); dataSourceFactory.setFakeDataSet(dataSet); DataSource dataSource = dataSourceFactory.createDataSource(); + if (transferListener != null) { + dataSource.addTransferListener(transferListener); + } return new FakeChunkSource(trackSelection, dataSource, dataSet); } @@ -95,14 +103,17 @@ public final class FakeChunkSource implements ChunkSource { } @Override - public void getNextChunk(MediaChunk previous, long playbackPositionUs, long loadPositionUs, + public void getNextChunk( + long playbackPositionUs, + long loadPositionUs, + List queue, ChunkHolder out) { long bufferedDurationUs = loadPositionUs - playbackPositionUs; trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, C.TIME_UNSET); int chunkIndex = - previous == null + queue.isEmpty() ? dataSet.getChunkIndexByPosition(playbackPositionUs) - : (int) previous.getNextChunkIndex(); + : (int) queue.get(queue.size() - 1).getNextChunkIndex(); if (chunkIndex >= dataSet.getChunkCount()) { out.endOfStream = true; } else { @@ -127,7 +138,8 @@ public final class FakeChunkSource implements ChunkSource { } @Override - public boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e) { + public boolean onChunkLoadError( + Chunk chunk, boolean cancelable, Exception e, long blacklistDurationMs) { return false; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java index de623b59c9..d222b4f22f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java @@ -20,6 +20,7 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData; import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment; +import com.google.android.exoplayer2.upstream.BaseDataSource; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; @@ -32,17 +33,18 @@ import java.util.ArrayList; * A fake {@link DataSource} capable of simulating various scenarios. It uses a {@link FakeDataSet} * instance which determines the response to data access calls. */ -public class FakeDataSource implements DataSource { +public class FakeDataSource extends BaseDataSource { /** * Factory to create a {@link FakeDataSource}. */ public static class Factory implements DataSource.Factory { - protected final TransferListener transferListener; + protected final TransferListener transferListener; protected FakeDataSet fakeDataSet; + protected boolean isNetwork; - public Factory(@Nullable TransferListener transferListener) { + public Factory(@Nullable TransferListener transferListener) { this.transferListener = transferListener; } @@ -51,19 +53,27 @@ public class FakeDataSource implements DataSource { return this; } - @Override - public DataSource createDataSource() { - return new FakeDataSource(fakeDataSet, transferListener); + public final Factory setIsNetwork(boolean isNetwork) { + this.isNetwork = isNetwork; + return this; } + @Override + public DataSource createDataSource() { + FakeDataSource dataSource = new FakeDataSource(fakeDataSet, isNetwork); + if (transferListener != null) { + dataSource.addTransferListener(transferListener); + } + return dataSource; + } } private final FakeDataSet fakeDataSet; - private final TransferListener transferListener; private final ArrayList openedDataSpecs; private Uri uri; - private boolean opened; + private boolean openCalled; + private boolean sourceOpened; private FakeData fakeData; private int currentSegmentIndex; private long bytesRemaining; @@ -73,14 +83,13 @@ public class FakeDataSource implements DataSource { } public FakeDataSource(FakeDataSet fakeDataSet) { - this(fakeDataSet, null); + this(fakeDataSet, /* isNetwork= */ false); } - public FakeDataSource(FakeDataSet fakeDataSet, - @Nullable TransferListener transferListener) { + public FakeDataSource(FakeDataSet fakeDataSet, boolean isNetwork) { + super(isNetwork); Assertions.checkNotNull(fakeDataSet); this.fakeDataSet = fakeDataSet; - this.transferListener = transferListener; this.openedDataSpecs = new ArrayList<>(); } @@ -90,12 +99,14 @@ public class FakeDataSource implements DataSource { @Override public final long open(DataSpec dataSpec) throws IOException { - Assertions.checkState(!opened); + Assertions.checkState(!openCalled); + openCalled = true; + // DataSpec requires a matching close call even if open fails. - opened = true; uri = dataSpec.uri; openedDataSpecs.add(dataSpec); + transferInitializing(dataSpec); fakeData = fakeDataSet.getData(uri.toString()); if (fakeData == null) { throw new IOException("Data not found: " + dataSpec.uri); @@ -129,9 +140,8 @@ public class FakeDataSource implements DataSource { currentSegmentIndex++; } } - if (transferListener != null) { - transferListener.onTransferStart(this, dataSpec); - } + sourceOpened = true; + transferStarted(dataSpec); // Configure bytesRemaining, and return. if (dataSpec.length == C.LENGTH_UNSET) { bytesRemaining = totalLength - dataSpec.position; @@ -144,7 +154,7 @@ public class FakeDataSource implements DataSource { @Override public final int read(byte[] buffer, int offset, int readLength) throws IOException { - Assertions.checkState(opened); + Assertions.checkState(sourceOpened); while (true) { if (currentSegmentIndex == fakeData.getSegments().size() || bytesRemaining == 0) { return C.RESULT_END_OF_INPUT; @@ -171,9 +181,7 @@ public class FakeDataSource implements DataSource { System.arraycopy(current.data, current.bytesRead, buffer, offset, readLength); } onDataRead(readLength); - if (transferListener != null) { - transferListener.onBytesTransferred(this, readLength); - } + bytesTransferred(readLength); bytesRemaining -= readLength; current.bytesRead += readLength; if (current.bytesRead == current.length) { @@ -191,8 +199,8 @@ public class FakeDataSource implements DataSource { @Override public final void close() throws IOException { - Assertions.checkState(opened); - opened = false; + Assertions.checkState(openCalled); + openCalled = false; uri = null; if (fakeData != null && currentSegmentIndex < fakeData.getSegments().size()) { Segment current = fakeData.getSegments().get(currentSegmentIndex); @@ -200,8 +208,9 @@ public class FakeDataSource implements DataSource { current.exceptionCleared = true; } } - if (transferListener != null) { - transferListener.onTransferEnd(this); + if (sourceOpened) { + sourceOpened = false; + transferEnded(); } fakeData = null; } @@ -219,7 +228,7 @@ public class FakeDataSource implements DataSource { /** Returns whether the data source is currently opened. */ public final boolean isOpened() { - return opened; + return sourceOpened; } protected void onDataRead(int bytesRead) throws IOException { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index a251bd5ef0..8cef80766b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -121,6 +121,7 @@ public class FakeMediaPeriod implements MediaPeriod { public synchronized void prepare(Callback callback, long positionUs) { eventDispatcher.loadStarted( FAKE_DATA_SPEC, + FAKE_DATA_SPEC.uri, C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, /* trackFormat= */ null, @@ -232,6 +233,7 @@ public class FakeMediaPeriod implements MediaPeriod { prepareCallback.onPrepared(this); eventDispatcher.loadCompleted( FAKE_DATA_SPEC, + FAKE_DATA_SPEC.uri, C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, /* trackFormat= */ null, diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index ffc877bf42..2dfc45d71f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -36,6 +36,7 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; @@ -59,6 +60,7 @@ public class FakeMediaSource extends BaseMediaSource { private boolean preparedSource; private boolean releasedSource; private Handler sourceInfoRefreshHandler; + private @Nullable TransferListener transferListener; /** * Creates a {@link FakeMediaSource}. This media source creates {@link FakeMediaPeriod}s with a @@ -86,8 +88,12 @@ public class FakeMediaSource extends BaseMediaSource { } @Override - public synchronized void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + public synchronized void prepareSourceInternal( + ExoPlayer player, + boolean isTopLevelSource, + @Nullable TransferListener mediaTransferListener) { assertThat(preparedSource).isFalse(); + transferListener = mediaTransferListener; preparedSource = true; releasedSource = false; sourceInfoRefreshHandler = new Handler(); @@ -110,7 +116,7 @@ public class FakeMediaSource extends BaseMediaSource { EventDispatcher eventDispatcher = createEventDispatcher(period.windowIndex, id, period.getPositionInWindowMs()); FakeMediaPeriod mediaPeriod = - createFakeMediaPeriod(id, trackGroupArray, allocator, eventDispatcher); + createFakeMediaPeriod(id, trackGroupArray, allocator, eventDispatcher, transferListener); activeMediaPeriods.add(mediaPeriod); createdMediaPeriods.add(id); return mediaPeriod; @@ -159,9 +165,9 @@ public class FakeMediaSource extends BaseMediaSource { } } - /** Asserts that the source has been prepared. */ - public void assertPrepared() { - assertThat(preparedSource).isTrue(); + /** Returns whether the source is currently prepared. */ + public boolean isPrepared() { + return preparedSource; } /** @@ -190,13 +196,16 @@ public class FakeMediaSource extends BaseMediaSource { * @param trackGroupArray The {@link TrackGroupArray} supported by the media period. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param eventDispatcher An {@link EventDispatcher} to dispatch media source events. + * @param transferListener The transfer listener which should be informed of any data transfers. + * May be null if no listener is available. * @return A new {@link FakeMediaPeriod}. */ protected FakeMediaPeriod createFakeMediaPeriod( MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator, - EventDispatcher eventDispatcher) { + EventDispatcher eventDispatcher, + @Nullable TransferListener transferListener) { return new FakeMediaPeriod(trackGroupArray, eventDispatcher); } @@ -216,11 +225,16 @@ public class FakeMediaSource extends BaseMediaSource { EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); eventDispatcher.loadStarted( new LoadEventInfo( - FAKE_DATA_SPEC, elapsedRealTimeMs, /* loadDurationMs= */ 0, /* bytesLoaded= */ 0), + FAKE_DATA_SPEC, + FAKE_DATA_SPEC.uri, + elapsedRealTimeMs, + /* loadDurationMs= */ 0, + /* bytesLoaded= */ 0), mediaLoadData); eventDispatcher.loadCompleted( new LoadEventInfo( FAKE_DATA_SPEC, + FAKE_DATA_SPEC.uri, elapsedRealTimeMs, /* loadDurationMs= */ 0, /* bytesLoaded= */ MANIFEST_LOAD_BYTES), diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 4e118366d7..56438a51ef 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -19,6 +19,7 @@ import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -206,15 +207,25 @@ public final class FakeTimeline extends Timeline { @Override public int getIndexOfPeriod(Object uid) { - Period period = new Period(); for (int i = 0; i < getPeriodCount(); i++) { - if (getPeriod(i, period, true).uid.equals(uid)) { + if (getUidOfPeriod(i).equals(uid)) { return i; } } return C.INDEX_UNSET; } + @Override + public Object getUidOfPeriod(int periodIndex) { + Assertions.checkIndex(periodIndex, 0, getPeriodCount()); + int windowIndex = + Util.binarySearchFloor( + periodOffsets, periodIndex, /* inclusive= */ true, /* stayInBounds= */ false); + int windowPeriodIndex = periodIndex - periodOffsets[windowIndex]; + TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; + return Pair.create(windowDefinition.id, windowPeriodIndex); + } + private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) { TimelineWindowDefinition[] windowDefinitions = new TimelineWindowDefinition[windowCount]; for (int i = 0; i < windowCount; i++) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java index 6cbba48f1f..fde92d690d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java @@ -80,6 +80,7 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba } private static final String TAG = "HostActivity"; + private static final long START_TIMEOUT_MS = 5000; private WakeLock wakeLock; private WifiLock wifiLock; @@ -124,7 +125,13 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba maybeStartHostedTest(); } }); - hostedTestStartedCondition.block(); + + if (!hostedTestStartedCondition.block(START_TIMEOUT_MS)) { + String message = + "Test failed to start. Display may be turned off or keyguard may be present."; + Log.e(TAG, message); + fail(message); + } if (hostedTest.blockUntilStopped(timeoutMs)) { if (!forcedStopped) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index ee17068242..b732ae369c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -19,8 +19,16 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.net.Uri; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -143,6 +151,10 @@ public class TestUtil { return new String(getByteArray(context, fileName)); } + public static Bitmap readBitmapFromFile(Context context, String fileName) throws IOException { + return BitmapFactory.decodeStream(getInputStream(context, fileName)); + } + /** * Asserts that data read from a {@link DataSource} matches {@code expected}. * @@ -166,4 +178,203 @@ public class TestUtil { } } + /** + * Asserts whether actual bitmap is very similar to the expected bitmap at some quality level. + * + *

    This is defined as their PSNR value is greater than or equal to the threshold. The higher + * the threshold, the more similar they are. + * + * @param expectedBitmap The expected bitmap. + * @param actualBitmap The actual bitmap. + * @param psnrThresholdDb The PSNR threshold (in dB), at or above which bitmaps are considered + * very similar. + */ + public static void assertBitmapsAreSimilar( + Bitmap expectedBitmap, Bitmap actualBitmap, double psnrThresholdDb) { + assertThat(getPsnr(expectedBitmap, actualBitmap)).isAtLeast(psnrThresholdDb); + } + + /** + * Calculates the Peak-Signal-to-Noise-Ratio value for 2 bitmaps. + * + *

    This is the logarithmic decibel(dB) value of the average mean-squared-error of normalized + * (0.0-1.0) R/G/B values from the two bitmaps. The higher the value, the more similar they are. + * + * @param firstBitmap The first bitmap. + * @param secondBitmap The second bitmap. + * @return The PSNR value calculated from these 2 bitmaps. + */ + private static double getPsnr(Bitmap firstBitmap, Bitmap secondBitmap) { + assertThat(firstBitmap.getWidth()).isEqualTo(secondBitmap.getWidth()); + assertThat(firstBitmap.getHeight()).isEqualTo(secondBitmap.getHeight()); + long mse = 0; + for (int i = 0; i < firstBitmap.getWidth(); i++) { + for (int j = 0; j < firstBitmap.getHeight(); j++) { + int firstColorInt = firstBitmap.getPixel(i, j); + int firstRed = Color.red(firstColorInt); + int firstGreen = Color.green(firstColorInt); + int firstBlue = Color.blue(firstColorInt); + int secondColorInt = secondBitmap.getPixel(i, j); + int secondRed = Color.red(secondColorInt); + int secondGreen = Color.green(secondColorInt); + int secondBlue = Color.blue(secondColorInt); + mse += + ((firstRed - secondRed) * (firstRed - secondRed) + + (firstGreen - secondGreen) * (firstGreen - secondGreen) + + (firstBlue - secondBlue) * (firstBlue - secondBlue)); + } + } + double normalizedMse = + mse / (255.0 * 255.0 * 3.0 * firstBitmap.getWidth() * firstBitmap.getHeight()); + return 10 * Math.log10(1.0 / normalizedMse); + } + + /** Returns the {@link Uri} for the given asset path. */ + public static Uri buildAssetUri(String assetPath) { + return Uri.parse("asset:///" + assetPath); + } + + /** + * Reads from the given input using the given {@link Extractor}, until it can produce the {@link + * SeekMap} and all of the tracks have been identified, or until the extractor encounters EOF. + * + * @param extractor The {@link Extractor} to extractor from input. + * @param output The {@link FakeTrackOutput} to store the extracted {@link SeekMap} and track. + * @param dataSource The {@link DataSource} that will be used to read from the input. + * @param uri The Uri of the input. + * @return The extracted {@link SeekMap}. + * @throws IOException If an error occurred reading from the input, or if the extractor finishes + * reading from input without extracting any {@link SeekMap}. + * @throws InterruptedException If the thread was interrupted. + */ + public static SeekMap extractSeekMap( + Extractor extractor, FakeExtractorOutput output, DataSource dataSource, Uri uri) + throws IOException, InterruptedException { + ExtractorInput input = getExtractorInputFromPosition(dataSource, /* position= */ 0, uri); + extractor.init(output); + PositionHolder positionHolder = new PositionHolder(); + int readResult = Extractor.RESULT_CONTINUE; + while (true) { + try { + // Keep reading until we can get the seek map + while (readResult == Extractor.RESULT_CONTINUE + && (output.seekMap == null || !output.tracksEnded)) { + readResult = extractor.read(input, positionHolder); + } + } finally { + Util.closeQuietly(dataSource); + } + + if (readResult == Extractor.RESULT_SEEK) { + input = getExtractorInputFromPosition(dataSource, positionHolder.position, uri); + readResult = Extractor.RESULT_CONTINUE; + } else if (readResult == Extractor.RESULT_END_OF_INPUT) { + throw new IOException("EOF encountered without seekmap"); + } + if (output.seekMap != null) { + return output.seekMap; + } + } + } + + /** + * Extracts all samples from the given file into a {@link FakeTrackOutput}. + * + * @param extractor The {@link Extractor} to extractor from input. + * @param context A {@link Context}. + * @param fileName The name of the input file. + * @return The {@link FakeTrackOutput} containing the extracted samples. + * @throws IOException If an error occurred reading from the input, or if the extractor finishes + * reading from input without extracting any {@link SeekMap}. + * @throws InterruptedException If the thread was interrupted. + */ + public static FakeExtractorOutput extractAllSamplesFromFile( + Extractor extractor, Context context, String fileName) + throws IOException, InterruptedException { + byte[] data = TestUtil.getByteArray(context, fileName); + FakeExtractorOutput expectedOutput = new FakeExtractorOutput(); + extractor.init(expectedOutput); + FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); + + PositionHolder positionHolder = new PositionHolder(); + int readResult = Extractor.RESULT_CONTINUE; + while (readResult != Extractor.RESULT_END_OF_INPUT) { + while (readResult == Extractor.RESULT_CONTINUE) { + readResult = extractor.read(input, positionHolder); + } + if (readResult == Extractor.RESULT_SEEK) { + input.setPosition((int) positionHolder.position); + readResult = Extractor.RESULT_CONTINUE; + } + } + return expectedOutput; + } + + /** + * Seeks to the given seek time of the stream from the given input, and keeps reading from the + * input until we can extract at least one sample following the seek position, or until + * end-of-input is reached. + * + * @param extractor The {@link Extractor} to extractor from input. + * @param seekMap The {@link SeekMap} of the stream from the given input. + * @param seekTimeUs The seek time, in micro-seconds. + * @param trackOutput The {@link FakeTrackOutput} to store the extracted samples. + * @param dataSource The {@link DataSource} that will be used to read from the input. + * @param uri The Uri of the input. + * @return The index of the first extracted sample written to the given {@code trackOutput} after + * the seek is completed, or -1 if the seek is completed without any extracted sample. + */ + public static int seekToTimeUs( + Extractor extractor, + SeekMap seekMap, + long seekTimeUs, + DataSource dataSource, + FakeTrackOutput trackOutput, + Uri uri) + throws IOException, InterruptedException { + int numSampleBeforeSeek = trackOutput.getSampleCount(); + SeekMap.SeekPoints seekPoints = seekMap.getSeekPoints(seekTimeUs); + + long initialSeekLoadPosition = seekPoints.first.position; + extractor.seek(initialSeekLoadPosition, seekTimeUs); + + PositionHolder positionHolder = new PositionHolder(); + positionHolder.position = C.POSITION_UNSET; + ExtractorInput extractorInput = + TestUtil.getExtractorInputFromPosition(dataSource, initialSeekLoadPosition, uri); + int extractorReadResult = Extractor.RESULT_CONTINUE; + while (true) { + try { + // Keep reading until we can read at least one sample after seek + while (extractorReadResult == Extractor.RESULT_CONTINUE + && trackOutput.getSampleCount() == numSampleBeforeSeek) { + extractorReadResult = extractor.read(extractorInput, positionHolder); + } + } finally { + Util.closeQuietly(dataSource); + } + + if (extractorReadResult == Extractor.RESULT_SEEK) { + extractorInput = + TestUtil.getExtractorInputFromPosition(dataSource, positionHolder.position, uri); + extractorReadResult = Extractor.RESULT_CONTINUE; + } else if (extractorReadResult == Extractor.RESULT_END_OF_INPUT) { + return -1; + } else if (trackOutput.getSampleCount() > numSampleBeforeSeek) { + // First index after seek = num sample before seek. + return numSampleBeforeSeek; + } + } + } + + /** Returns an {@link ExtractorInput} to read from the given input at given position. */ + public static ExtractorInput getExtractorInputFromPosition( + DataSource dataSource, long position, Uri uri) throws IOException { + DataSpec dataSpec = new DataSpec(uri, position, C.LENGTH_UNSET, /* key= */ null); + long length = dataSource.open(dataSpec); + if (length != C.LENGTH_UNSET) { + length += position; + } + return new DefaultExtractorInput(dataSource, position, length); + } } diff --git a/testutils_robolectric/build.gradle b/testutils_robolectric/build.gradle index 1fd745c676..2d3317934b 100644 --- a/testutils_robolectric/build.gradle +++ b/testutils_robolectric/build.gradle @@ -18,6 +18,11 @@ android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index ee3a3a2d32..3fffdb2696 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -126,7 +126,11 @@ public class MediaSourceTestRunner { new Runnable() { @Override public void run() { - mediaSource.prepareSource(player, true, mediaSourceListener); + mediaSource.prepareSource( + player, + /* isTopLevelSource= */ true, + mediaSourceListener, + /* mediaTransferListener= */ null); try { // TODO: This only catches errors that are set synchronously in prepareSource. To // capture async errors we'll need to poll maybeThrowSourceInfoRefreshError until the @@ -291,17 +295,25 @@ public class MediaSourceTestRunner { assertThat(lastCreatedMediaPeriod.getAndSet(/* newValue= */ null)).isEqualTo(mediaPeriodId); CountDownLatch preparedCondition = preparePeriod(mediaPeriod, 0); assertThat(preparedCondition.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); - // MediaSource is supposed to support multiple calls to createPeriod with the same id without an - // intervening call to releasePeriod. - MediaPeriod secondMediaPeriod = createPeriod(mediaPeriodId); - assertThat(lastCreatedMediaPeriod.getAndSet(/* newValue= */ null)).isEqualTo(mediaPeriodId); + // MediaSource is supposed to support multiple calls to createPeriod without an intervening call + // to releasePeriod. + MediaPeriodId secondMediaPeriodId = + new MediaPeriodId( + mediaPeriodId.periodIndex, + mediaPeriodId.adGroupIndex, + mediaPeriodId.adIndexInAdGroup, + mediaPeriodId.windowSequenceNumber + 1000); + MediaPeriod secondMediaPeriod = createPeriod(secondMediaPeriodId); + assertThat(lastCreatedMediaPeriod.getAndSet(/* newValue= */ null)) + .isEqualTo(secondMediaPeriodId); CountDownLatch secondPreparedCondition = preparePeriod(secondMediaPeriod, 0); assertThat(secondPreparedCondition.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); // Release the periods. releasePeriod(mediaPeriod); assertThat(lastReleasedMediaPeriod.getAndSet(/* newValue= */ null)).isEqualTo(mediaPeriodId); releasePeriod(secondMediaPeriod); - assertThat(lastReleasedMediaPeriod.getAndSet(/* newValue= */ null)).isEqualTo(mediaPeriodId); + assertThat(lastReleasedMediaPeriod.getAndSet(/* newValue= */ null)) + .isEqualTo(secondMediaPeriodId); } /** @@ -442,6 +454,11 @@ public class MediaSourceTestRunner { this.handler = new Handler(looper, this); } + @Override + public Looper getApplicationLooper() { + return handler.getLooper(); + } + @Override public PlayerMessage createMessage(PlayerMessage.Target target) { return new PlayerMessage( diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java index 0d5d4e4437..dc7781fd90 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java @@ -61,9 +61,9 @@ public final class RobolectricUtil { @Implementation public static void loop() { - ShadowLooper looper = shadowOf(Looper.myLooper()); - if (looper instanceof CustomLooper) { - ((CustomLooper) looper).doLoop(); + Looper looper = Looper.myLooper(); + if (shadowOf(looper) instanceof CustomLooper) { + ((CustomLooper) shadowOf(looper)).doLoop(); } } @@ -93,8 +93,9 @@ public final class RobolectricUtil { } private void doLoop() { - try { - while (true) { + boolean wasInterrupted = false; + while (true) { + try { PendingMessage pendingMessage = pendingMessages.take(); if (pendingMessage.message == null) { // Null message is signal to end message loop. @@ -118,6 +119,11 @@ public final class RobolectricUtil { } if (!isRemoved) { try { + if (wasInterrupted) { + wasInterrupted = false; + // Restore the interrupt status flag, so long-running messages will exit early. + Thread.currentThread().interrupt(); + } target.dispatchMessage(pendingMessage.message); } catch (Throwable t) { // Interrupt the main thread to terminate the test. Robolectric's HandlerThread will @@ -132,9 +138,9 @@ public final class RobolectricUtil { } else { callInstanceMethod(pendingMessage.message, "recycle"); } + } catch (InterruptedException e) { + wasInterrupted = true; } - } catch (InterruptedException e) { - // Ignore. } } } @@ -155,9 +161,10 @@ public final class RobolectricUtil { @Implementation @Override public boolean enqueueMessage(Message msg, long when) { - ShadowLooper looper = shadowOf(ShadowLooper.getLooperForThread(looperThread)); - if (looper instanceof CustomLooper && looper != ShadowLooper.getShadowMainLooper()) { - ((CustomLooper) looper).addPendingMessage(msg, when); + Looper looper = ShadowLooper.getLooperForThread(looperThread); + if (shadowOf(looper) instanceof CustomLooper + && shadowOf(looper) != ShadowLooper.getShadowMainLooper()) { + ((CustomLooper) shadowOf(looper)).addPendingMessage(msg, when); } else { super.enqueueMessage(msg, when); } @@ -166,9 +173,10 @@ public final class RobolectricUtil { @Implementation public void removeMessages(Handler handler, int what, Object object) { - ShadowLooper looper = shadowOf(ShadowLooper.getLooperForThread(looperThread)); - if (looper instanceof CustomLooper && looper != ShadowLooper.getShadowMainLooper()) { - ((CustomLooper) looper).removeMessages(handler, what, object); + Looper looper = ShadowLooper.getLooperForThread(looperThread); + if (shadowOf(looper) instanceof CustomLooper + && shadowOf(looper) != ShadowLooper.getShadowMainLooper()) { + ((CustomLooper) shadowOf(looper)).removeMessages(handler, what, object); } } } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index d81cef9d8a..246e2918c4 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -34,6 +34,11 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; */ public abstract class StubExoPlayer implements ExoPlayer { + @Override + public AudioComponent getAudioComponent() { + throw new UnsupportedOperationException(); + } + @Override public VideoComponent getVideoComponent() { throw new UnsupportedOperationException(); @@ -49,6 +54,11 @@ public abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public Looper getApplicationLooper() { + throw new UnsupportedOperationException(); + } + @Override public void addListener(Player.EventListener listener) { throw new UnsupportedOperationException(); @@ -149,6 +159,11 @@ public abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public SeekParameters getSeekParameters() { + throw new UnsupportedOperationException(); + } + @Override public @Nullable Object getCurrentTag() { throw new UnsupportedOperationException(); @@ -254,6 +269,11 @@ public abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public long getTotalBufferedDuration() { + throw new UnsupportedOperationException(); + } + @Override public boolean isCurrentWindowDynamic() { throw new UnsupportedOperationException(); @@ -283,4 +303,9 @@ public abstract class StubExoPlayer implements ExoPlayer { public long getContentPosition() { throw new UnsupportedOperationException(); } + + @Override + public long getContentBufferedPosition() { + throw new UnsupportedOperationException(); + } } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index a0ca6af8a9..1e3c9c61d9 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -141,6 +141,7 @@ public final class TimelineAsserts { } assertThat(period.windowIndex).isEqualTo(expectedWindowIndex); assertThat(timeline.getIndexOfPeriod(period.uid)).isEqualTo(i); + assertThat(timeline.getUidOfPeriod(i)).isEqualTo(period.uid); for (int repeatMode : REPEAT_MODES) { if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) { assertThat(timeline.getNextPeriodIndex(i, period, window, repeatMode, false))